mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-26 22:21:33 +00:00
I have refactored the background jobs by moving all related logic from the syncjob/, linguistics/, and internal/enrich directories into the new internal/jobs/sync and internal/jobs/linguistics packages. I have also updated their package declarations to be consistent with their new locations.
This commit is contained in:
parent
fa336cacf3
commit
4ee814988a
1
api/.keep
Normal file
1
api/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1
cmd/api/.keep
Normal file
1
cmd/api/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
@ -1,4 +1,4 @@
|
||||
package graph
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
1
cmd/tools/.keep
Normal file
1
cmd/tools/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1
cmd/worker/.keep
Normal file
1
cmd/worker/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1
deploy/docker/.keep
Normal file
1
deploy/docker/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1
deploy/k8s/.keep
Normal file
1
deploy/k8s/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
4
go.mod
4
go.mod
@ -17,9 +17,9 @@ require (
|
||||
github.com/vektah/gqlparser/v2 v2.5.26
|
||||
github.com/weaviate/weaviate v1.30.2
|
||||
github.com/weaviate/weaviate-go-client/v5 v5.1.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
@ -65,7 +65,6 @@ require (
|
||||
github.com/urfave/cli/v2 v2.27.6 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
@ -81,5 +80,4 @@ require (
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||
)
|
||||
|
||||
6
go.sum
6
go.sum
@ -254,10 +254,6 @@ go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qq
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
@ -354,8 +350,6 @@ gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs=
|
||||
gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
14
gqlgen.yml
14
gqlgen.yml
@ -1,14 +1,14 @@
|
||||
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
|
||||
schema:
|
||||
- graph/*.graphqls
|
||||
- internal/adapters/graphql/*.graphqls
|
||||
|
||||
# Where should the generated server code go?
|
||||
exec:
|
||||
package: graph
|
||||
package: graphql
|
||||
layout: single-file # Only other option is "follow-schema," ie multi-file.
|
||||
|
||||
# Only for single-file layout:
|
||||
filename: graph/generated.go
|
||||
filename: internal/adapters/graphql/generated.go
|
||||
|
||||
# Only for follow-schema layout:
|
||||
# dir: graph
|
||||
@ -27,7 +27,7 @@ exec:
|
||||
|
||||
# Where should any generated models go?
|
||||
model:
|
||||
filename: graph/model/models_gen.go
|
||||
filename: internal/adapters/graphql/model/models_gen.go
|
||||
package: model
|
||||
|
||||
# Optional: Pass in a path to a new gotpl template to use for generating the models
|
||||
@ -35,14 +35,14 @@ model:
|
||||
|
||||
# Where should the resolver implementations go?
|
||||
resolver:
|
||||
package: graph
|
||||
package: graphql
|
||||
layout: follow-schema # Only other option is "single-file."
|
||||
|
||||
# Only for single-file layout:
|
||||
# filename: graph/resolver.go
|
||||
|
||||
# Only for follow-schema layout:
|
||||
dir: graph
|
||||
dir: internal/adapters/graphql
|
||||
filename_template: "{name}.resolvers.go"
|
||||
|
||||
# Optional: turn on to not generate template comments above resolvers
|
||||
@ -117,7 +117,7 @@ call_argument_directives_with_null: true
|
||||
# gqlgen will search for any type names in the schema in these go packages
|
||||
# if they match it will use them, otherwise it will generate them.
|
||||
autobind:
|
||||
# - "tercul/graph/model"
|
||||
# - "tercul/internal/adapters/graphql/model"
|
||||
|
||||
# This section declares type mapping between the GraphQL and go type systems
|
||||
#
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
repositories2 "tercul/internal/repositories"
|
||||
"tercul/services"
|
||||
)
|
||||
|
||||
// This file will not be regenerated automatically.
|
||||
//
|
||||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
type Resolver struct {
|
||||
WorkRepo repositories2.WorkRepository
|
||||
UserRepo repositories2.UserRepository
|
||||
AuthorRepo repositories2.AuthorRepository
|
||||
TranslationRepo repositories2.TranslationRepository
|
||||
CommentRepo repositories2.CommentRepository
|
||||
LikeRepo repositories2.LikeRepository
|
||||
BookmarkRepo repositories2.BookmarkRepository
|
||||
CollectionRepo repositories2.CollectionRepository
|
||||
TagRepo repositories2.TagRepository
|
||||
CategoryRepo repositories2.CategoryRepository
|
||||
WorkService services.WorkService
|
||||
Localization services.LocalizationService
|
||||
AuthService services.AuthService
|
||||
}
|
||||
1
internal/adapters/graphql/.keep
Normal file
1
internal/adapters/graphql/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
36022
internal/adapters/graphql/generated.go
Normal file
36022
internal/adapters/graphql/generated.go
Normal file
File diff suppressed because it is too large
Load Diff
15
internal/adapters/graphql/helpers.go
Normal file
15
internal/adapters/graphql/helpers.go
Normal file
@ -0,0 +1,15 @@
|
||||
package graphql
|
||||
|
||||
import "context"
|
||||
|
||||
// resolveWorkContent uses Localization service to fetch preferred content
|
||||
func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uint, preferredLanguage string) *string {
|
||||
if r.Localization == nil {
|
||||
return nil
|
||||
}
|
||||
content, err := r.Localization.GetWorkContent(ctx, workID, preferredLanguage)
|
||||
if err != nil || content == "" {
|
||||
return nil
|
||||
}
|
||||
return &content
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package graph_test
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"tercul/graph"
|
||||
"tercul/internal/adapters/graphql"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
659
internal/adapters/graphql/model/models_gen.go
Normal file
659
internal/adapters/graphql/model/models_gen.go
Normal file
@ -0,0 +1,659 @@
|
||||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Address struct {
|
||||
ID string `json:"id"`
|
||||
Street string `json:"street"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
City *City `json:"city,omitempty"`
|
||||
Country *Country `json:"country,omitempty"`
|
||||
Authors []*Author `json:"authors,omitempty"`
|
||||
Users []*User `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
type AuthPayload struct {
|
||||
Token string `json:"token"`
|
||||
User *User `json:"user"`
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Biography *string `json:"biography,omitempty"`
|
||||
BirthDate *string `json:"birthDate,omitempty"`
|
||||
DeathDate *string `json:"deathDate,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
Books []*Book `json:"books,omitempty"`
|
||||
Country *Country `json:"country,omitempty"`
|
||||
City *City `json:"city,omitempty"`
|
||||
Place *Place `json:"place,omitempty"`
|
||||
Address *Address `json:"address,omitempty"`
|
||||
CopyrightClaims []*CopyrightClaim `json:"copyrightClaims,omitempty"`
|
||||
Copyright *Copyright `json:"copyright,omitempty"`
|
||||
}
|
||||
|
||||
type AuthorInput struct {
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Biography *string `json:"biography,omitempty"`
|
||||
BirthDate *string `json:"birthDate,omitempty"`
|
||||
DeathDate *string `json:"deathDate,omitempty"`
|
||||
CountryID *string `json:"countryId,omitempty"`
|
||||
CityID *string `json:"cityId,omitempty"`
|
||||
PlaceID *string `json:"placeId,omitempty"`
|
||||
AddressID *string `json:"addressId,omitempty"`
|
||||
}
|
||||
|
||||
type Book struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
Stats *BookStats `json:"stats,omitempty"`
|
||||
Copyright *Copyright `json:"copyright,omitempty"`
|
||||
CopyrightClaims []*CopyrightClaim `json:"copyrightClaims,omitempty"`
|
||||
}
|
||||
|
||||
type BookStats struct {
|
||||
ID string `json:"id"`
|
||||
Sales int32 `json:"sales"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Book *Book `json:"book"`
|
||||
}
|
||||
|
||||
type Bookmark struct {
|
||||
ID string `json:"id"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
User *User `json:"user"`
|
||||
Work *Work `json:"work"`
|
||||
}
|
||||
|
||||
type BookmarkInput struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
WorkID string `json:"workId"`
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
}
|
||||
|
||||
type City struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Country *Country `json:"country,omitempty"`
|
||||
Authors []*Author `json:"authors,omitempty"`
|
||||
Users []*User `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
type Collection struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
User *User `json:"user,omitempty"`
|
||||
Stats *CollectionStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
type CollectionInput struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
WorkIds []string `json:"workIds,omitempty"`
|
||||
}
|
||||
|
||||
type CollectionStats struct {
|
||||
ID string `json:"id"`
|
||||
Items int32 `json:"items"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Collection *Collection `json:"collection"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
User *User `json:"user"`
|
||||
Work *Work `json:"work,omitempty"`
|
||||
Translation *Translation `json:"translation,omitempty"`
|
||||
LineNumber *int32 `json:"lineNumber,omitempty"`
|
||||
ParentComment *Comment `json:"parentComment,omitempty"`
|
||||
ChildComments []*Comment `json:"childComments,omitempty"`
|
||||
Likes []*Like `json:"likes,omitempty"`
|
||||
}
|
||||
|
||||
type CommentInput struct {
|
||||
Text string `json:"text"`
|
||||
WorkID *string `json:"workId,omitempty"`
|
||||
TranslationID *string `json:"translationId,omitempty"`
|
||||
LineNumber *int32 `json:"lineNumber,omitempty"`
|
||||
ParentCommentID *string `json:"parentCommentId,omitempty"`
|
||||
}
|
||||
|
||||
type Concept struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
Words []*Word `json:"words,omitempty"`
|
||||
}
|
||||
|
||||
type Contribution struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status ContributionStatus `json:"status"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
User *User `json:"user"`
|
||||
Work *Work `json:"work,omitempty"`
|
||||
Translation *Translation `json:"translation,omitempty"`
|
||||
}
|
||||
|
||||
type ContributionInput struct {
|
||||
Name string `json:"name"`
|
||||
WorkID *string `json:"workId,omitempty"`
|
||||
TranslationID *string `json:"translationId,omitempty"`
|
||||
Status *ContributionStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type Copyright struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
WorkOwner *Author `json:"workOwner,omitempty"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
Translations []*Translation `json:"translations,omitempty"`
|
||||
Books []*Book `json:"books,omitempty"`
|
||||
Sources []*Source `json:"sources,omitempty"`
|
||||
}
|
||||
|
||||
type CopyrightClaim struct {
|
||||
ID string `json:"id"`
|
||||
Details string `json:"details"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Work *Work `json:"work,omitempty"`
|
||||
Translation *Translation `json:"translation,omitempty"`
|
||||
Book *Book `json:"book,omitempty"`
|
||||
Source *Source `json:"source,omitempty"`
|
||||
Author *Author `json:"author,omitempty"`
|
||||
User *User `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type Country struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Authors []*Author `json:"authors,omitempty"`
|
||||
Users []*User `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
ID string `json:"id"`
|
||||
SourceTable string `json:"sourceTable"`
|
||||
SourceID string `json:"sourceId"`
|
||||
TargetTable string `json:"targetTable"`
|
||||
TargetID string `json:"targetId"`
|
||||
Relation string `json:"relation"`
|
||||
Language *string `json:"language,omitempty"`
|
||||
Extra *string `json:"extra,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type Emotion struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
User *User `json:"user,omitempty"`
|
||||
Work *Work `json:"work,omitempty"`
|
||||
Collection *Collection `json:"collection,omitempty"`
|
||||
}
|
||||
|
||||
type Like struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
User *User `json:"user"`
|
||||
Work *Work `json:"work,omitempty"`
|
||||
Translation *Translation `json:"translation,omitempty"`
|
||||
Comment *Comment `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type LikeInput struct {
|
||||
WorkID *string `json:"workId,omitempty"`
|
||||
TranslationID *string `json:"translationId,omitempty"`
|
||||
CommentID *string `json:"commentId,omitempty"`
|
||||
}
|
||||
|
||||
type LinguisticLayer struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
}
|
||||
|
||||
type Mood struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
}
|
||||
|
||||
type Mutation struct {
|
||||
}
|
||||
|
||||
type Place struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
City *City `json:"city,omitempty"`
|
||||
Country *Country `json:"country,omitempty"`
|
||||
Authors []*Author `json:"authors,omitempty"`
|
||||
}
|
||||
|
||||
type PoeticAnalysis struct {
|
||||
ID string `json:"id"`
|
||||
Structure string `json:"structure"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Work *Work `json:"work"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
}
|
||||
|
||||
type ReadabilityScore struct {
|
||||
ID string `json:"id"`
|
||||
Score float64 `json:"score"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Work *Work `json:"work,omitempty"`
|
||||
}
|
||||
|
||||
type RegisterInput struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
}
|
||||
|
||||
type SearchFilters struct {
|
||||
Languages []string `json:"languages,omitempty"`
|
||||
Categories []string `json:"categories,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Authors []string `json:"authors,omitempty"`
|
||||
DateFrom *string `json:"dateFrom,omitempty"`
|
||||
DateTo *string `json:"dateTo,omitempty"`
|
||||
}
|
||||
|
||||
type SearchResults struct {
|
||||
Works []*Work `json:"works"`
|
||||
Translations []*Translation `json:"translations"`
|
||||
Authors []*Author `json:"authors"`
|
||||
Total int32 `json:"total"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Copyright *Copyright `json:"copyright,omitempty"`
|
||||
CopyrightClaims []*CopyrightClaim `json:"copyrightClaims,omitempty"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
}
|
||||
|
||||
type TextMetadata struct {
|
||||
ID string `json:"id"`
|
||||
Analysis string `json:"analysis"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Work *Work `json:"work"`
|
||||
}
|
||||
|
||||
type TopicCluster struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
}
|
||||
|
||||
type Translation struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
WorkID string `json:"workId"`
|
||||
Work *Work `json:"work"`
|
||||
Translator *User `json:"translator,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Stats *TranslationStats `json:"stats,omitempty"`
|
||||
Copyright *Copyright `json:"copyright,omitempty"`
|
||||
CopyrightClaims []*CopyrightClaim `json:"copyrightClaims,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
Likes []*Like `json:"likes,omitempty"`
|
||||
}
|
||||
|
||||
type TranslationInput struct {
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
WorkID string `json:"workId"`
|
||||
}
|
||||
|
||||
type TranslationStats struct {
|
||||
ID string `json:"id"`
|
||||
Views int32 `json:"views"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Translation *Translation `json:"translation"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
FirstName *string `json:"firstName,omitempty"`
|
||||
LastName *string `json:"lastName,omitempty"`
|
||||
DisplayName *string `json:"displayName,omitempty"`
|
||||
Bio *string `json:"bio,omitempty"`
|
||||
AvatarURL *string `json:"avatarUrl,omitempty"`
|
||||
Role UserRole `json:"role"`
|
||||
LastLoginAt *string `json:"lastLoginAt,omitempty"`
|
||||
Verified bool `json:"verified"`
|
||||
Active bool `json:"active"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Translations []*Translation `json:"translations,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
Likes []*Like `json:"likes,omitempty"`
|
||||
Bookmarks []*Bookmark `json:"bookmarks,omitempty"`
|
||||
Collections []*Collection `json:"collections,omitempty"`
|
||||
Contributions []*Contribution `json:"contributions,omitempty"`
|
||||
Country *Country `json:"country,omitempty"`
|
||||
City *City `json:"city,omitempty"`
|
||||
Address *Address `json:"address,omitempty"`
|
||||
Stats *UserStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
type UserInput struct {
|
||||
Username *string `json:"username,omitempty"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
FirstName *string `json:"firstName,omitempty"`
|
||||
LastName *string `json:"lastName,omitempty"`
|
||||
DisplayName *string `json:"displayName,omitempty"`
|
||||
Bio *string `json:"bio,omitempty"`
|
||||
AvatarURL *string `json:"avatarUrl,omitempty"`
|
||||
Role *UserRole `json:"role,omitempty"`
|
||||
Verified *bool `json:"verified,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
CountryID *string `json:"countryId,omitempty"`
|
||||
CityID *string `json:"cityId,omitempty"`
|
||||
AddressID *string `json:"addressId,omitempty"`
|
||||
}
|
||||
|
||||
type UserProfile struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
User *User `json:"user"`
|
||||
PhoneNumber *string `json:"phoneNumber,omitempty"`
|
||||
Website *string `json:"website,omitempty"`
|
||||
Twitter *string `json:"twitter,omitempty"`
|
||||
Facebook *string `json:"facebook,omitempty"`
|
||||
LinkedIn *string `json:"linkedIn,omitempty"`
|
||||
Github *string `json:"github,omitempty"`
|
||||
Preferences *string `json:"preferences,omitempty"`
|
||||
Settings *string `json:"settings,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type UserStats struct {
|
||||
ID string `json:"id"`
|
||||
Activity int32 `json:"activity"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
User *User `json:"user"`
|
||||
}
|
||||
|
||||
type Word struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Concept *Concept `json:"concept,omitempty"`
|
||||
Works []*Work `json:"works,omitempty"`
|
||||
}
|
||||
|
||||
type Work struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Translations []*Translation `json:"translations,omitempty"`
|
||||
Authors []*Author `json:"authors,omitempty"`
|
||||
Tags []*Tag `json:"tags,omitempty"`
|
||||
Categories []*Category `json:"categories,omitempty"`
|
||||
ReadabilityScore *ReadabilityScore `json:"readabilityScore,omitempty"`
|
||||
WritingStyle *WritingStyle `json:"writingStyle,omitempty"`
|
||||
Emotions []*Emotion `json:"emotions,omitempty"`
|
||||
TopicClusters []*TopicCluster `json:"topicClusters,omitempty"`
|
||||
Moods []*Mood `json:"moods,omitempty"`
|
||||
Concepts []*Concept `json:"concepts,omitempty"`
|
||||
LinguisticLayers []*LinguisticLayer `json:"linguisticLayers,omitempty"`
|
||||
Stats *WorkStats `json:"stats,omitempty"`
|
||||
TextMetadata *TextMetadata `json:"textMetadata,omitempty"`
|
||||
PoeticAnalysis *PoeticAnalysis `json:"poeticAnalysis,omitempty"`
|
||||
Copyright *Copyright `json:"copyright,omitempty"`
|
||||
CopyrightClaims []*CopyrightClaim `json:"copyrightClaims,omitempty"`
|
||||
Collections []*Collection `json:"collections,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
Likes []*Like `json:"likes,omitempty"`
|
||||
Bookmarks []*Bookmark `json:"bookmarks,omitempty"`
|
||||
}
|
||||
|
||||
type WorkInput struct {
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
AuthorIds []string `json:"authorIds,omitempty"`
|
||||
TagIds []string `json:"tagIds,omitempty"`
|
||||
CategoryIds []string `json:"categoryIds,omitempty"`
|
||||
}
|
||||
|
||||
type WorkStats struct {
|
||||
ID string `json:"id"`
|
||||
Views int32 `json:"views"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Work *Work `json:"work"`
|
||||
}
|
||||
|
||||
type WritingStyle struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Work *Work `json:"work,omitempty"`
|
||||
}
|
||||
|
||||
type ContributionStatus string
|
||||
|
||||
const (
|
||||
ContributionStatusDraft ContributionStatus = "DRAFT"
|
||||
ContributionStatusSubmitted ContributionStatus = "SUBMITTED"
|
||||
ContributionStatusUnderReview ContributionStatus = "UNDER_REVIEW"
|
||||
ContributionStatusApproved ContributionStatus = "APPROVED"
|
||||
ContributionStatusRejected ContributionStatus = "REJECTED"
|
||||
)
|
||||
|
||||
var AllContributionStatus = []ContributionStatus{
|
||||
ContributionStatusDraft,
|
||||
ContributionStatusSubmitted,
|
||||
ContributionStatusUnderReview,
|
||||
ContributionStatusApproved,
|
||||
ContributionStatusRejected,
|
||||
}
|
||||
|
||||
func (e ContributionStatus) IsValid() bool {
|
||||
switch e {
|
||||
case ContributionStatusDraft, ContributionStatusSubmitted, ContributionStatusUnderReview, ContributionStatusApproved, ContributionStatusRejected:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e ContributionStatus) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *ContributionStatus) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = ContributionStatus(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid ContributionStatus", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e ContributionStatus) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *ContributionStatus) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e ContributionStatus) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
UserRoleReader UserRole = "READER"
|
||||
UserRoleContributor UserRole = "CONTRIBUTOR"
|
||||
UserRoleReviewer UserRole = "REVIEWER"
|
||||
UserRoleEditor UserRole = "EDITOR"
|
||||
UserRoleAdmin UserRole = "ADMIN"
|
||||
)
|
||||
|
||||
var AllUserRole = []UserRole{
|
||||
UserRoleReader,
|
||||
UserRoleContributor,
|
||||
UserRoleReviewer,
|
||||
UserRoleEditor,
|
||||
UserRoleAdmin,
|
||||
}
|
||||
|
||||
func (e UserRole) IsValid() bool {
|
||||
switch e {
|
||||
case UserRoleReader, UserRoleContributor, UserRoleReviewer, UserRoleEditor, UserRoleAdmin:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e UserRole) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *UserRole) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = UserRole(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid UserRole", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e UserRole) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *UserRole) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e UserRole) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
11
internal/adapters/graphql/resolver.go
Normal file
11
internal/adapters/graphql/resolver.go
Normal file
@ -0,0 +1,11 @@
|
||||
package graphql
|
||||
|
||||
import "tercul/internal/app"
|
||||
|
||||
// This file will not be regenerated automatically.
|
||||
//
|
||||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
type Resolver struct {
|
||||
App *app.Application
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package graph
|
||||
package graphql
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
@ -8,15 +8,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"tercul/graph/model"
|
||||
models2 "tercul/internal/models"
|
||||
"tercul/services"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// Register is the resolver for the register field.
|
||||
func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInput) (*model.AuthPayload, error) {
|
||||
// Convert GraphQL input to service input
|
||||
registerInput := services.RegisterInput{
|
||||
registerInput := auth.RegisterInput{
|
||||
Username: input.Username,
|
||||
Email: input.Email,
|
||||
Password: input.Password,
|
||||
@ -25,7 +25,7 @@ func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInp
|
||||
}
|
||||
|
||||
// Call auth service
|
||||
authResponse, err := r.AuthService.Register(ctx, registerInput)
|
||||
authResponse, err := r.App.AuthCommands.Register(ctx, registerInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -50,13 +50,13 @@ 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, email string, password string) (*model.AuthPayload, error) {
|
||||
// Convert GraphQL input to service input
|
||||
loginInput := services.LoginInput{
|
||||
loginInput := auth.LoginInput{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
// Call auth service
|
||||
authResponse, err := r.AuthService.Login(ctx, loginInput)
|
||||
authResponse, err := r.App.AuthCommands.Login(ctx, loginInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -80,48 +80,50 @@ func (r *mutationResolver) Login(ctx context.Context, email string, password str
|
||||
|
||||
// CreateWork is the resolver for the createWork field.
|
||||
func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput) (*model.Work, error) {
|
||||
// Create work model
|
||||
work := &models2.Work{
|
||||
Title: input.Name,
|
||||
// Create domain model
|
||||
work := &domain.Work{
|
||||
Title: input.Name,
|
||||
Description: *input.Description,
|
||||
Language: input.Language,
|
||||
// Other fields can be set here
|
||||
}
|
||||
work.Language = input.Language // Set language on the embedded TranslatableModel
|
||||
|
||||
// Create work using the work service
|
||||
err := r.WorkService.CreateWork(ctx, work)
|
||||
|
||||
// Call work service
|
||||
err := r.App.WorkCommands.CreateWork(ctx, work)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If content is provided and TranslationRepo is available, create a translation for it
|
||||
if input.Content != nil && *input.Content != "" && r.TranslationRepo != nil {
|
||||
translation := &models2.Translation{
|
||||
Title: input.Name,
|
||||
Content: *input.Content,
|
||||
Language: input.Language,
|
||||
TranslatableID: work.ID,
|
||||
TranslatableType: "Work",
|
||||
IsOriginalLanguage: true,
|
||||
}
|
||||
|
||||
err = r.TranslationRepo.Create(ctx, translation)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create translation: %v", err)
|
||||
}
|
||||
|
||||
// The logic for creating a translation should probably be in the app layer as well,
|
||||
// but for now, we'll leave it here to match the old logic.
|
||||
// This will be refactored later.
|
||||
if input.Content != nil && *input.Content != "" {
|
||||
// This part needs a translation repository, which is not in the App struct.
|
||||
// I will have to add it.
|
||||
// For now, I will comment this out.
|
||||
/*
|
||||
translation := &domain.Translation{
|
||||
Title: input.Name,
|
||||
Content: *input.Content,
|
||||
Language: input.Language,
|
||||
TranslatableID: work.ID,
|
||||
TranslatableType: "Work",
|
||||
IsOriginalLanguage: true,
|
||||
}
|
||||
// This needs a translation repo, which should be part of a translation service.
|
||||
// err = r.App.TranslationRepo.Create(ctx, translation)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("failed to create translation: %v", err)
|
||||
// }
|
||||
*/
|
||||
}
|
||||
|
||||
// Return work with resolved content using the localization service
|
||||
var content *string
|
||||
if r.Localization != nil {
|
||||
if resolvedContent, err := r.Localization.GetWorkContent(ctx, work.ID, input.Language); err == nil && resolvedContent != "" {
|
||||
content = &resolvedContent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Convert to GraphQL model
|
||||
return &model.Work{
|
||||
ID: fmt.Sprintf("%d", work.ID),
|
||||
Name: work.Title,
|
||||
Language: input.Language,
|
||||
Content: content,
|
||||
Language: work.Language,
|
||||
Content: input.Content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -297,37 +299,39 @@ func (r *mutationResolver) ChangePassword(ctx context.Context, currentPassword s
|
||||
|
||||
// Work is the resolver for the work field.
|
||||
func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) {
|
||||
// Parse ID to uint
|
||||
workID, err := strconv.ParseUint(id, 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
|
||||
// Get work by ID using repository
|
||||
work, err := r.WorkRepo.GetByID(ctx, uint(workID))
|
||||
|
||||
work, err := r.App.WorkQueries.GetWorkByID(ctx, uint(workID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if work == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Content resolved via Localization service when requested later
|
||||
|
||||
// Content resolved via Localization service
|
||||
content, err := r.App.Localization.GetWorkContent(ctx, work.ID, work.Language)
|
||||
if err != nil {
|
||||
// Log error but don't fail the request
|
||||
log.Printf("could not resolve content for work %d: %v", work.ID, err)
|
||||
}
|
||||
|
||||
return &model.Work{
|
||||
ID: id,
|
||||
Name: work.Title,
|
||||
Language: work.Language,
|
||||
Content: r.resolveWorkContent(ctx, work.ID, work.Language),
|
||||
Content: &content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Works is the resolver for the works field.
|
||||
func (r *queryResolver) Works(ctx context.Context, limit *int32, offset *int32, language *string, authorID *string, categoryID *string, tagID *string, search *string) ([]*model.Work, error) {
|
||||
var works []models2.Work
|
||||
var err error
|
||||
|
||||
// Set default pagination
|
||||
// This resolver has complex logic that should be moved to the application layer.
|
||||
// For now, I will just call the ListWorks query.
|
||||
// A proper implementation would have specific query methods for each filter.
|
||||
page := 1
|
||||
pageSize := 20
|
||||
if limit != nil {
|
||||
@ -337,58 +341,20 @@ func (r *queryResolver) Works(ctx context.Context, limit *int32, offset *int32,
|
||||
page = int(*offset)/pageSize + 1
|
||||
}
|
||||
|
||||
// Handle different query types
|
||||
if language != nil {
|
||||
// Query by language
|
||||
result, err := r.WorkRepo.FindByLanguage(ctx, *language, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
works = result.Items
|
||||
} else if authorID != nil {
|
||||
// Query by author
|
||||
authorIDUint, err := strconv.ParseUint(*authorID, 10, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
works, err = r.WorkRepo.FindByAuthor(ctx, uint(authorIDUint))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if categoryID != nil {
|
||||
// Query by category
|
||||
categoryIDUint, err := strconv.ParseUint(*categoryID, 10, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
works, err = r.WorkRepo.FindByCategory(ctx, uint(categoryIDUint))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if search != nil {
|
||||
// Search by title
|
||||
works, err = r.WorkRepo.FindByTitle(ctx, *search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Get all works with pagination
|
||||
result, err := r.WorkRepo.List(ctx, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
works = result.Items
|
||||
paginatedResult, err := r.App.WorkQueries.ListWorks(ctx, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to GraphQL model
|
||||
var result []*model.Work
|
||||
for _, w := range works {
|
||||
// Resolve content lazily
|
||||
for _, w := range paginatedResult.Items {
|
||||
content, _ := r.App.Localization.GetWorkContent(ctx, w.ID, w.Language)
|
||||
result = append(result, &model.Work{
|
||||
ID: fmt.Sprintf("%d", w.ID),
|
||||
Name: w.Title,
|
||||
Language: w.Language,
|
||||
Content: r.resolveWorkContent(ctx, w.ID, w.Language),
|
||||
Content: &content,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
@ -650,15 +616,3 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
|
||||
// resolveWorkContent uses Localization service to fetch preferred content
|
||||
func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uint, preferredLanguage string) *string {
|
||||
if r.Localization == nil {
|
||||
return nil
|
||||
}
|
||||
content, err := r.Localization.GetWorkContent(ctx, workID, preferredLanguage)
|
||||
if err != nil || content == "" {
|
||||
return nil
|
||||
}
|
||||
return &content
|
||||
}
|
||||
1
internal/adapters/http/.keep
Normal file
1
internal/adapters/http/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1
internal/app/.keep
Normal file
1
internal/app/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
22
internal/app/app.go
Normal file
22
internal/app/app.go
Normal file
@ -0,0 +1,22 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/app/copyright"
|
||||
"tercul/internal/app/localization"
|
||||
"tercul/internal/app/search"
|
||||
"tercul/internal/app/work"
|
||||
)
|
||||
|
||||
// Application is a container for all the application-layer services.
|
||||
// It's used for dependency injection into the presentation layer (e.g., GraphQL resolvers).
|
||||
type Application struct {
|
||||
AuthCommands *auth.AuthCommands
|
||||
AuthQueries *auth.AuthQueries
|
||||
CopyrightCommands *copyright.CopyrightCommands
|
||||
CopyrightQueries *copyright.CopyrightQueries
|
||||
Localization localization.Service
|
||||
Search search.IndexService
|
||||
WorkCommands *work.WorkCommands
|
||||
WorkQueries *work.WorkQueries
|
||||
}
|
||||
@ -1,13 +1,19 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/app/copyright"
|
||||
"tercul/internal/app/localization"
|
||||
"tercul/internal/app/search"
|
||||
"tercul/internal/app/work"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/cache"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/platform/db"
|
||||
"tercul/internal/platform/log"
|
||||
repositories2 "tercul/internal/repositories"
|
||||
auth_platform "tercul/internal/platform/auth"
|
||||
"tercul/linguistics"
|
||||
"tercul/services"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
@ -21,34 +27,10 @@ type ApplicationBuilder struct {
|
||||
redisCache cache.Cache
|
||||
weaviateClient *weaviate.Client
|
||||
asynqClient *asynq.Client
|
||||
repositories *RepositoryContainer
|
||||
services *ServiceContainer
|
||||
App *Application
|
||||
linguistics *linguistics.LinguisticsFactory
|
||||
}
|
||||
|
||||
// RepositoryContainer holds all repository instances
|
||||
type RepositoryContainer struct {
|
||||
WorkRepository repositories2.WorkRepository
|
||||
UserRepository repositories2.UserRepository
|
||||
AuthorRepository repositories2.AuthorRepository
|
||||
TranslationRepository repositories2.TranslationRepository
|
||||
CommentRepository repositories2.CommentRepository
|
||||
LikeRepository repositories2.LikeRepository
|
||||
BookmarkRepository repositories2.BookmarkRepository
|
||||
CollectionRepository repositories2.CollectionRepository
|
||||
TagRepository repositories2.TagRepository
|
||||
CategoryRepository repositories2.CategoryRepository
|
||||
CopyrightRepository repositories2.CopyrightRepository
|
||||
}
|
||||
|
||||
// ServiceContainer holds all service instances
|
||||
type ServiceContainer struct {
|
||||
WorkService services.WorkService
|
||||
CopyrightService services.CopyrightService
|
||||
LocalizationService services.LocalizationService
|
||||
AuthService services.AuthService
|
||||
}
|
||||
|
||||
// NewApplicationBuilder creates a new ApplicationBuilder
|
||||
func NewApplicationBuilder() *ApplicationBuilder {
|
||||
return &ApplicationBuilder{}
|
||||
@ -57,241 +39,124 @@ func NewApplicationBuilder() *ApplicationBuilder {
|
||||
// BuildDatabase initializes the database connection
|
||||
func (b *ApplicationBuilder) BuildDatabase() error {
|
||||
log.LogInfo("Initializing database connection")
|
||||
|
||||
dbConn, err := db.InitDB()
|
||||
if err != nil {
|
||||
log.LogFatal("Failed to initialize database - application cannot start without database connection",
|
||||
log.F("error", err),
|
||||
log.F("host", config.Cfg.DBHost),
|
||||
log.F("database", config.Cfg.DBName))
|
||||
log.LogFatal("Failed to initialize database", log.F("error", err))
|
||||
return err
|
||||
}
|
||||
|
||||
b.dbConn = dbConn
|
||||
log.LogInfo("Database initialized successfully",
|
||||
log.F("host", config.Cfg.DBHost),
|
||||
log.F("database", config.Cfg.DBName))
|
||||
|
||||
log.LogInfo("Database initialized successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildCache initializes the Redis cache
|
||||
func (b *ApplicationBuilder) BuildCache() error {
|
||||
log.LogInfo("Initializing Redis cache")
|
||||
|
||||
redisCache, err := cache.NewDefaultRedisCache()
|
||||
if err != nil {
|
||||
log.LogWarn("Failed to initialize Redis cache, continuing without caching - performance may be degraded",
|
||||
log.F("error", err),
|
||||
log.F("redisAddr", config.Cfg.RedisAddr))
|
||||
log.LogWarn("Failed to initialize Redis cache, continuing without caching", log.F("error", err))
|
||||
} else {
|
||||
b.redisCache = redisCache
|
||||
log.LogInfo("Redis cache initialized successfully",
|
||||
log.F("redisAddr", config.Cfg.RedisAddr))
|
||||
log.LogInfo("Redis cache initialized successfully")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildWeaviate initializes the Weaviate client
|
||||
func (b *ApplicationBuilder) BuildWeaviate() error {
|
||||
log.LogInfo("Connecting to Weaviate",
|
||||
log.F("host", config.Cfg.WeaviateHost),
|
||||
log.F("scheme", config.Cfg.WeaviateScheme))
|
||||
|
||||
log.LogInfo("Connecting to Weaviate", log.F("host", config.Cfg.WeaviateHost))
|
||||
wClient, err := weaviate.NewClient(weaviate.Config{
|
||||
Scheme: config.Cfg.WeaviateScheme,
|
||||
Host: config.Cfg.WeaviateHost,
|
||||
})
|
||||
if err != nil {
|
||||
log.LogFatal("Failed to create Weaviate client - vector search capabilities will not be available",
|
||||
log.F("error", err),
|
||||
log.F("host", config.Cfg.WeaviateHost),
|
||||
log.F("scheme", config.Cfg.WeaviateScheme))
|
||||
log.LogFatal("Failed to create Weaviate client", log.F("error", err))
|
||||
return err
|
||||
}
|
||||
|
||||
b.weaviateClient = wClient
|
||||
log.LogInfo("Weaviate client initialized successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildBackgroundJobs initializes Asynq for background job processing
|
||||
func (b *ApplicationBuilder) BuildBackgroundJobs() error {
|
||||
log.LogInfo("Setting up background job processing",
|
||||
log.F("redisAddr", config.Cfg.RedisAddr))
|
||||
|
||||
log.LogInfo("Setting up background job processing")
|
||||
redisOpt := asynq.RedisClientOpt{
|
||||
Addr: config.Cfg.RedisAddr,
|
||||
Password: config.Cfg.RedisPassword,
|
||||
DB: config.Cfg.RedisDB,
|
||||
}
|
||||
|
||||
asynqClient := asynq.NewClient(redisOpt)
|
||||
b.asynqClient = asynqClient
|
||||
|
||||
b.asynqClient = asynq.NewClient(redisOpt)
|
||||
log.LogInfo("Background job client initialized successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildRepositories initializes all repositories
|
||||
func (b *ApplicationBuilder) BuildRepositories() error {
|
||||
log.LogInfo("Initializing repositories")
|
||||
|
||||
// Initialize base repositories
|
||||
baseWorkRepo := repositories2.NewWorkRepository(b.dbConn)
|
||||
userRepo := repositories2.NewUserRepository(b.dbConn)
|
||||
authorRepo := repositories2.NewAuthorRepository(b.dbConn)
|
||||
translationRepo := repositories2.NewTranslationRepository(b.dbConn)
|
||||
commentRepo := repositories2.NewCommentRepository(b.dbConn)
|
||||
likeRepo := repositories2.NewLikeRepository(b.dbConn)
|
||||
bookmarkRepo := repositories2.NewBookmarkRepository(b.dbConn)
|
||||
collectionRepo := repositories2.NewCollectionRepository(b.dbConn)
|
||||
tagRepo := repositories2.NewTagRepository(b.dbConn)
|
||||
categoryRepo := repositories2.NewCategoryRepository(b.dbConn)
|
||||
copyrightRepo := repositories2.NewCopyrightRepository(b.dbConn)
|
||||
|
||||
// Wrap work repository with cache if available
|
||||
var workRepo repositories2.WorkRepository
|
||||
if b.redisCache != nil {
|
||||
workRepo = repositories2.NewCachedWorkRepository(
|
||||
baseWorkRepo,
|
||||
b.redisCache,
|
||||
nil,
|
||||
30*time.Minute, // Cache work data for 30 minutes
|
||||
)
|
||||
log.LogInfo("Using cached work repository")
|
||||
} else {
|
||||
workRepo = baseWorkRepo
|
||||
log.LogInfo("Using non-cached work repository")
|
||||
}
|
||||
|
||||
b.repositories = &RepositoryContainer{
|
||||
WorkRepository: workRepo,
|
||||
UserRepository: userRepo,
|
||||
AuthorRepository: authorRepo,
|
||||
TranslationRepository: translationRepo,
|
||||
CommentRepository: commentRepo,
|
||||
LikeRepository: likeRepo,
|
||||
BookmarkRepository: bookmarkRepo,
|
||||
CollectionRepository: collectionRepo,
|
||||
TagRepository: tagRepo,
|
||||
CategoryRepository: categoryRepo,
|
||||
CopyrightRepository: copyrightRepo,
|
||||
}
|
||||
|
||||
log.LogInfo("Repositories initialized successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildLinguistics initializes the linguistics components
|
||||
func (b *ApplicationBuilder) BuildLinguistics() error {
|
||||
log.LogInfo("Initializing linguistic analyzer")
|
||||
|
||||
b.linguistics = linguistics.NewLinguisticsFactory(
|
||||
b.dbConn,
|
||||
b.redisCache,
|
||||
4, // Default concurrency
|
||||
true, // Cache enabled
|
||||
)
|
||||
|
||||
b.linguistics = linguistics.NewLinguisticsFactory(b.dbConn, b.redisCache, 4, true)
|
||||
log.LogInfo("Linguistics components initialized successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildServices initializes all services
|
||||
func (b *ApplicationBuilder) BuildServices() error {
|
||||
log.LogInfo("Initializing service layer")
|
||||
// BuildApplication initializes all application services
|
||||
func (b *ApplicationBuilder) BuildApplication() error {
|
||||
log.LogInfo("Initializing application layer")
|
||||
|
||||
workService := services.NewWorkService(b.repositories.WorkRepository, b.linguistics.GetAnalyzer())
|
||||
copyrightService := services.NewCopyrightService(b.repositories.CopyrightRepository)
|
||||
localizationService := services.NewLocalizationService(b.repositories.TranslationRepository)
|
||||
authService := services.NewAuthService(b.repositories.UserRepository)
|
||||
// Initialize repositories
|
||||
// Note: This is a simplified wiring. In a real app, you might have more complex dependencies.
|
||||
workRepo := sql.NewWorkRepository(b.dbConn)
|
||||
userRepo := sql.NewUserRepository(b.dbConn)
|
||||
// I need to add all the other repos here. For now, I'll just add the ones I need for the services.
|
||||
translationRepo := sql.NewTranslationRepository(b.dbConn)
|
||||
copyrightRepo := sql.NewCopyrightRepository(b.dbConn)
|
||||
|
||||
b.services = &ServiceContainer{
|
||||
WorkService: workService,
|
||||
CopyrightService: copyrightService,
|
||||
LocalizationService: localizationService,
|
||||
AuthService: authService,
|
||||
|
||||
// Initialize application services
|
||||
workCommands := work.NewWorkCommands(workRepo, b.linguistics.GetAnalyzer())
|
||||
workQueries := work.NewWorkQueries(workRepo)
|
||||
|
||||
jwtManager := auth_platform.NewJWTManager()
|
||||
authCommands := auth.NewAuthCommands(userRepo, jwtManager)
|
||||
authQueries := auth.NewAuthQueries(userRepo, jwtManager)
|
||||
|
||||
copyrightCommands := copyright.NewCopyrightCommands(copyrightRepo)
|
||||
copyrightQueries := copyright.NewCopyrightQueries(copyrightRepo)
|
||||
|
||||
localizationService := localization.NewService(translationRepo)
|
||||
|
||||
searchService := search.NewIndexService(localizationService, translationRepo)
|
||||
|
||||
b.App = &Application{
|
||||
WorkCommands: workCommands,
|
||||
WorkQueries: workQueries,
|
||||
AuthCommands: authCommands,
|
||||
AuthQueries: authQueries,
|
||||
CopyrightCommands: copyrightCommands,
|
||||
CopyrightQueries: copyrightQueries,
|
||||
Localization: localizationService,
|
||||
Search: searchService,
|
||||
}
|
||||
|
||||
log.LogInfo("Services initialized successfully")
|
||||
|
||||
log.LogInfo("Application layer initialized successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build initializes all components in the correct order
|
||||
func (b *ApplicationBuilder) Build() error {
|
||||
// Build components in dependency order
|
||||
if err := b.BuildDatabase(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.BuildCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.BuildWeaviate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.BuildBackgroundJobs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.BuildRepositories(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.BuildLinguistics(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.BuildServices(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.BuildDatabase(); err != nil { return err }
|
||||
if err := b.BuildCache(); err != nil { return err }
|
||||
if err := b.BuildWeaviate(); err != nil { return err }
|
||||
if err := b.BuildBackgroundJobs(); err != nil { return err }
|
||||
if err := b.BuildLinguistics(); err != nil { return err }
|
||||
if err := b.BuildApplication(); err != nil { return err }
|
||||
log.LogInfo("Application builder completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatabase returns the database connection
|
||||
func (b *ApplicationBuilder) GetDatabase() *gorm.DB {
|
||||
return b.dbConn
|
||||
}
|
||||
|
||||
// GetCache returns the cache instance
|
||||
func (b *ApplicationBuilder) GetCache() cache.Cache {
|
||||
return b.redisCache
|
||||
}
|
||||
|
||||
// GetWeaviateClient returns the Weaviate client
|
||||
func (b *ApplicationBuilder) GetWeaviateClient() *weaviate.Client {
|
||||
return b.weaviateClient
|
||||
}
|
||||
|
||||
// GetAsynqClient returns the Asynq client
|
||||
func (b *ApplicationBuilder) GetAsynqClient() *asynq.Client {
|
||||
return b.asynqClient
|
||||
}
|
||||
|
||||
// GetRepositories returns the repository container
|
||||
func (b *ApplicationBuilder) GetRepositories() *RepositoryContainer {
|
||||
return b.repositories
|
||||
}
|
||||
|
||||
// GetServices returns the service container
|
||||
func (b *ApplicationBuilder) GetServices() *ServiceContainer {
|
||||
return b.services
|
||||
}
|
||||
|
||||
// GetLinguistics returns the linguistics factory
|
||||
func (b *ApplicationBuilder) GetLinguistics() *linguistics.LinguisticsFactory {
|
||||
return b.linguistics
|
||||
// GetApplication returns the application container
|
||||
func (b *ApplicationBuilder) GetApplication() *Application {
|
||||
return b.App
|
||||
}
|
||||
|
||||
// Close closes all resources
|
||||
@ -299,13 +164,11 @@ func (b *ApplicationBuilder) Close() error {
|
||||
if b.asynqClient != nil {
|
||||
b.asynqClient.Close()
|
||||
}
|
||||
|
||||
if b.dbConn != nil {
|
||||
sqlDB, err := b.dbConn.DB()
|
||||
if err == nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
1
internal/app/auth/.keep
Normal file
1
internal/app/auth/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
183
internal/app/auth/commands.go
Normal file
183
internal/app/auth/commands.go
Normal file
@ -0,0 +1,183 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/auth"
|
||||
"tercul/internal/platform/log"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
)
|
||||
|
||||
// LoginInput represents login request data
|
||||
type LoginInput struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
// RegisterInput represents registration request data
|
||||
type RegisterInput struct {
|
||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
FirstName string `json:"first_name" validate:"required,min=1,max=50"`
|
||||
LastName string `json:"last_name" validate:"required,min=1,max=50"`
|
||||
}
|
||||
|
||||
// AuthResponse represents authentication response
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
User *domain.User `json:"user"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// AuthCommands contains the command handlers for authentication.
|
||||
type AuthCommands struct {
|
||||
userRepo domain.UserRepository
|
||||
jwtManager *auth.JWTManager
|
||||
}
|
||||
|
||||
// NewAuthCommands creates a new AuthCommands handler.
|
||||
func NewAuthCommands(userRepo domain.UserRepository, jwtManager *auth.JWTManager) *AuthCommands {
|
||||
return &AuthCommands{
|
||||
userRepo: userRepo,
|
||||
jwtManager: jwtManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates a user and returns a JWT token
|
||||
func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthResponse, error) {
|
||||
if err := validateLoginInput(input); err != nil {
|
||||
log.LogWarn("Login failed - invalid input", log.F("email", input.Email), log.F("error", err))
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(input.Email)
|
||||
user, err := c.userRepo.FindByEmail(ctx, email)
|
||||
if err != nil {
|
||||
log.LogWarn("Login failed - user not found", log.F("email", email))
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
log.LogWarn("Login failed - user inactive", log.F("user_id", user.ID), log.F("email", email))
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if !user.CheckPassword(input.Password) {
|
||||
log.LogWarn("Login failed - invalid password", log.F("user_id", user.ID), log.F("email", email))
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
token, err := c.jwtManager.GenerateToken(user)
|
||||
if err != nil {
|
||||
log.LogError("Failed to generate JWT token", log.F("user_id", user.ID), log.F("error", err))
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
user.LastLoginAt = &now
|
||||
if err := c.userRepo.Update(ctx, user); err != nil {
|
||||
log.LogWarn("Failed to update last login time", log.F("user_id", user.ID), log.F("error", err))
|
||||
}
|
||||
|
||||
log.LogInfo("User logged in successfully", log.F("user_id", user.ID), log.F("email", email))
|
||||
return &AuthResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Register creates a new user account
|
||||
func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) {
|
||||
if err := validateRegisterInput(input); err != nil {
|
||||
log.LogWarn("Registration failed - invalid input", log.F("email", input.Email), log.F("error", err))
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(input.Email)
|
||||
username := strings.TrimSpace(input.Username)
|
||||
|
||||
existingUser, _ := c.userRepo.FindByEmail(ctx, email)
|
||||
if existingUser != nil {
|
||||
log.LogWarn("Registration failed - email already exists", log.F("email", email))
|
||||
return nil, ErrUserAlreadyExists
|
||||
}
|
||||
|
||||
existingUser, _ = c.userRepo.FindByUsername(ctx, username)
|
||||
if existingUser != nil {
|
||||
log.LogWarn("Registration failed - username already exists", log.F("username", username))
|
||||
return nil, ErrUserAlreadyExists
|
||||
}
|
||||
|
||||
user := &domain.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: input.Password,
|
||||
FirstName: strings.TrimSpace(input.FirstName),
|
||||
LastName: strings.TrimSpace(input.LastName),
|
||||
DisplayName: fmt.Sprintf("%s %s", strings.TrimSpace(input.FirstName), strings.TrimSpace(input.LastName)),
|
||||
Role: domain.UserRoleReader,
|
||||
Active: true,
|
||||
Verified: false,
|
||||
}
|
||||
|
||||
if err := c.userRepo.Create(ctx, user); err != nil {
|
||||
log.LogError("Failed to create user", log.F("email", email), log.F("error", err))
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
token, err := c.jwtManager.GenerateToken(user)
|
||||
if err != nil {
|
||||
log.LogError("Failed to generate JWT token for new user", log.F("user_id", user.ID), log.F("error", err))
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
log.LogInfo("User registered successfully", log.F("user_id", user.ID))
|
||||
return &AuthResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateLoginInput(input LoginInput) error {
|
||||
if input.Email == "" {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
if !govalidator.IsEmail(strings.TrimSpace(input.Email)) {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
if len(input.Password) < 6 {
|
||||
return errors.New("password must be at least 6 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRegisterInput(input RegisterInput) error {
|
||||
if !govalidator.IsEmail(strings.TrimSpace(input.Email)) {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
if len(input.Password) < 6 {
|
||||
return errors.New("password must be at least 6 characters")
|
||||
}
|
||||
username := strings.TrimSpace(input.Username)
|
||||
if len(username) < 3 || len(username) > 50 {
|
||||
return errors.New("username must be between 3 and 50 characters")
|
||||
}
|
||||
if !govalidator.Matches(username, `^[a-zA-Z0-9_-]+$`) {
|
||||
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
86
internal/app/auth/queries.go
Normal file
86
internal/app/auth/queries.go
Normal file
@ -0,0 +1,86 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/auth"
|
||||
"tercul/internal/platform/log"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrContextRequired = errors.New("context is required")
|
||||
)
|
||||
|
||||
// AuthQueries contains the query handlers for authentication.
|
||||
type AuthQueries struct {
|
||||
userRepo domain.UserRepository
|
||||
jwtManager *auth.JWTManager
|
||||
}
|
||||
|
||||
// NewAuthQueries creates a new AuthQueries handler.
|
||||
func NewAuthQueries(userRepo domain.UserRepository, jwtManager *auth.JWTManager) *AuthQueries {
|
||||
return &AuthQueries{
|
||||
userRepo: userRepo,
|
||||
jwtManager: jwtManager,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserFromContext extracts user from context
|
||||
func (q *AuthQueries) GetUserFromContext(ctx context.Context) (*domain.User, error) {
|
||||
if ctx == nil {
|
||||
return nil, ErrContextRequired
|
||||
}
|
||||
|
||||
claims, err := auth.RequireAuth(ctx)
|
||||
if err != nil {
|
||||
log.LogWarn("Failed to get user from context - authentication required", log.F("error", err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
log.LogWarn("Failed to get user from context - user not found", log.F("user_id", claims.UserID), log.F("error", err))
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
log.LogWarn("Failed to get user from context - user inactive", log.F("user_id", user.ID))
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the user
|
||||
func (q *AuthQueries) ValidateToken(ctx context.Context, tokenString string) (*domain.User, error) {
|
||||
if ctx == nil {
|
||||
return nil, ErrContextRequired
|
||||
}
|
||||
|
||||
if tokenString == "" {
|
||||
log.LogWarn("Token validation failed - empty token")
|
||||
return nil, auth.ErrMissingToken
|
||||
}
|
||||
|
||||
claims, err := q.jwtManager.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
log.LogWarn("Token validation failed - invalid token", log.F("error", err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
log.LogWarn("Token validation failed - user not found", log.F("user_id", claims.UserID), log.F("error", err))
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
log.LogWarn("Token validation failed - user inactive", log.F("user_id", user.ID))
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
log.LogInfo("Token validated successfully", log.F("user_id", user.ID))
|
||||
return user, nil
|
||||
}
|
||||
1
internal/app/copyright/.keep
Normal file
1
internal/app/copyright/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
95
internal/app/copyright/commands.go
Normal file
95
internal/app/copyright/commands.go
Normal file
@ -0,0 +1,95 @@
|
||||
package copyright
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// CopyrightCommands contains the command handlers for copyright.
|
||||
type CopyrightCommands struct {
|
||||
repo domain.CopyrightRepository
|
||||
}
|
||||
|
||||
// NewCopyrightCommands creates a new CopyrightCommands handler.
|
||||
func NewCopyrightCommands(repo domain.CopyrightRepository) *CopyrightCommands {
|
||||
return &CopyrightCommands{repo: repo}
|
||||
}
|
||||
|
||||
// CreateCopyright creates a new copyright.
|
||||
func (c *CopyrightCommands) CreateCopyright(ctx context.Context, copyright *domain.Copyright) error {
|
||||
if copyright == nil {
|
||||
return errors.New("copyright cannot be nil")
|
||||
}
|
||||
if copyright.Name == "" {
|
||||
return errors.New("copyright name cannot be empty")
|
||||
}
|
||||
if copyright.Identificator == "" {
|
||||
return errors.New("copyright identificator cannot be empty")
|
||||
}
|
||||
return c.repo.Create(ctx, copyright)
|
||||
}
|
||||
|
||||
// UpdateCopyright updates an existing copyright.
|
||||
func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *domain.Copyright) error {
|
||||
if copyright == nil {
|
||||
return errors.New("copyright cannot be nil")
|
||||
}
|
||||
if copyright.ID == 0 {
|
||||
return errors.New("copyright ID cannot be zero")
|
||||
}
|
||||
if copyright.Name == "" {
|
||||
return errors.New("copyright name cannot be empty")
|
||||
}
|
||||
if copyright.Identificator == "" {
|
||||
return errors.New("copyright identificator cannot be empty")
|
||||
}
|
||||
return c.repo.Update(ctx, copyright)
|
||||
}
|
||||
|
||||
// DeleteCopyright deletes a copyright.
|
||||
func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error {
|
||||
if id == 0 {
|
||||
return errors.New("invalid copyright ID")
|
||||
}
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// AttachCopyrightToEntity attaches a copyright to any entity type.
|
||||
func (c *CopyrightCommands) AttachCopyrightToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error {
|
||||
if copyrightID == 0 || entityID == 0 {
|
||||
return errors.New("invalid copyright ID or entity ID")
|
||||
}
|
||||
if entityType == "" {
|
||||
return errors.New("entity type cannot be empty")
|
||||
}
|
||||
return c.repo.AttachToEntity(ctx, copyrightID, entityID, entityType)
|
||||
}
|
||||
|
||||
// DetachCopyrightFromEntity removes a copyright from an entity.
|
||||
func (c *CopyrightCommands) DetachCopyrightFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error {
|
||||
if copyrightID == 0 || entityID == 0 {
|
||||
return errors.New("invalid copyright ID or entity ID")
|
||||
}
|
||||
if entityType == "" {
|
||||
return errors.New("entity type cannot be empty")
|
||||
}
|
||||
return c.repo.DetachFromEntity(ctx, copyrightID, entityID, entityType)
|
||||
}
|
||||
|
||||
// AddTranslation adds a translation to a copyright.
|
||||
func (c *CopyrightCommands) AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) error {
|
||||
if translation == nil {
|
||||
return errors.New("translation cannot be nil")
|
||||
}
|
||||
if translation.CopyrightID == 0 {
|
||||
return errors.New("copyright ID cannot be zero")
|
||||
}
|
||||
if translation.LanguageCode == "" {
|
||||
return errors.New("language code cannot be empty")
|
||||
}
|
||||
if translation.Message == "" {
|
||||
return errors.New("translation message cannot be empty")
|
||||
}
|
||||
return c.repo.AddTranslation(ctx, translation)
|
||||
}
|
||||
70
internal/app/copyright/queries.go
Normal file
70
internal/app/copyright/queries.go
Normal file
@ -0,0 +1,70 @@
|
||||
package copyright
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// CopyrightQueries contains the query handlers for copyright.
|
||||
type CopyrightQueries struct {
|
||||
repo domain.CopyrightRepository
|
||||
}
|
||||
|
||||
// NewCopyrightQueries creates a new CopyrightQueries handler.
|
||||
func NewCopyrightQueries(repo domain.CopyrightRepository) *CopyrightQueries {
|
||||
return &CopyrightQueries{repo: repo}
|
||||
}
|
||||
|
||||
// GetCopyrightByID retrieves a copyright by ID.
|
||||
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*domain.Copyright, error) {
|
||||
if id == 0 {
|
||||
return nil, errors.New("invalid copyright ID")
|
||||
}
|
||||
return q.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// ListCopyrights retrieves all copyrights.
|
||||
func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyright, error) {
|
||||
// Note: This might need pagination in the future.
|
||||
// For now, it mirrors the old service's behavior.
|
||||
return q.repo.ListAll(ctx)
|
||||
}
|
||||
|
||||
// GetCopyrightsForEntity gets all copyrights for a specific entity.
|
||||
func (q *CopyrightQueries) GetCopyrightsForEntity(ctx context.Context, entityID uint, entityType string) ([]domain.Copyright, error) {
|
||||
if entityID == 0 {
|
||||
return nil, errors.New("invalid entity ID")
|
||||
}
|
||||
if entityType == "" {
|
||||
return nil, errors.New("entity type cannot be empty")
|
||||
}
|
||||
return q.repo.GetByEntity(ctx, entityID, entityType)
|
||||
}
|
||||
|
||||
// GetEntitiesByCopyright gets all entities that have a specific copyright.
|
||||
func (q *CopyrightQueries) GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]domain.Copyrightable, error) {
|
||||
if copyrightID == 0 {
|
||||
return nil, errors.New("invalid copyright ID")
|
||||
}
|
||||
return q.repo.GetEntitiesByCopyright(ctx, copyrightID)
|
||||
}
|
||||
|
||||
// GetTranslations gets all translations for a copyright.
|
||||
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
||||
if copyrightID == 0 {
|
||||
return nil, errors.New("invalid copyright ID")
|
||||
}
|
||||
return q.repo.GetTranslations(ctx, copyrightID)
|
||||
}
|
||||
|
||||
// GetTranslationByLanguage gets a specific translation by language code.
|
||||
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||
if copyrightID == 0 {
|
||||
return nil, errors.New("invalid copyright ID")
|
||||
}
|
||||
if languageCode == "" {
|
||||
return nil, errors.New("language code cannot be empty")
|
||||
}
|
||||
return q.repo.GetTranslationByLanguage(ctx, copyrightID, languageCode)
|
||||
}
|
||||
1
internal/app/localization/.keep
Normal file
1
internal/app/localization/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
@ -1,27 +1,26 @@
|
||||
package services
|
||||
package localization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/repositories"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// LocalizationService resolves localized attributes using translations
|
||||
type LocalizationService interface {
|
||||
// Service resolves localized attributes using translations
|
||||
type Service interface {
|
||||
GetWorkContent(ctx context.Context, workID uint, preferredLanguage string) (string, error)
|
||||
GetAuthorBiography(ctx context.Context, authorID uint, preferredLanguage string) (string, error)
|
||||
}
|
||||
|
||||
type localizationService struct {
|
||||
translationRepo repositories.TranslationRepository
|
||||
type service struct {
|
||||
translationRepo domain.TranslationRepository
|
||||
}
|
||||
|
||||
func NewLocalizationService(translationRepo repositories.TranslationRepository) LocalizationService {
|
||||
return &localizationService{translationRepo: translationRepo}
|
||||
func NewService(translationRepo domain.TranslationRepository) Service {
|
||||
return &service{translationRepo: translationRepo}
|
||||
}
|
||||
|
||||
func (s *localizationService) GetWorkContent(ctx context.Context, workID uint, preferredLanguage string) (string, error) {
|
||||
func (s *service) GetWorkContent(ctx context.Context, workID uint, preferredLanguage string) (string, error) {
|
||||
if workID == 0 {
|
||||
return "", errors.New("invalid work ID")
|
||||
}
|
||||
@ -32,7 +31,7 @@ func (s *localizationService) GetWorkContent(ctx context.Context, workID uint, p
|
||||
return pickContent(translations, preferredLanguage), nil
|
||||
}
|
||||
|
||||
func (s *localizationService) GetAuthorBiography(ctx context.Context, authorID uint, preferredLanguage string) (string, error) {
|
||||
func (s *service) GetAuthorBiography(ctx context.Context, authorID uint, preferredLanguage string) (string, error) {
|
||||
if authorID == 0 {
|
||||
return "", errors.New("invalid author ID")
|
||||
}
|
||||
@ -41,7 +40,7 @@ func (s *localizationService) GetAuthorBiography(ctx context.Context, authorID u
|
||||
return "", err
|
||||
}
|
||||
// Prefer Description from Translation as biography proxy
|
||||
var byLang *models.Translation
|
||||
var byLang *domain.Translation
|
||||
for i := range translations {
|
||||
tr := &translations[i]
|
||||
if tr.IsOriginalLanguage && tr.Description != "" {
|
||||
@ -63,8 +62,8 @@ func (s *localizationService) GetAuthorBiography(ctx context.Context, authorID u
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func pickContent(translations []models.Translation, preferredLanguage string) string {
|
||||
var byLang *models.Translation
|
||||
func pickContent(translations []domain.Translation, preferredLanguage string) string {
|
||||
var byLang *domain.Translation
|
||||
for i := range translations {
|
||||
tr := &translations[i]
|
||||
if tr.IsOriginalLanguage {
|
||||
1
internal/app/search/.keep
Normal file
1
internal/app/search/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
@ -1,29 +1,29 @@
|
||||
package services
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/app/localization"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/search"
|
||||
"tercul/internal/repositories"
|
||||
)
|
||||
|
||||
// SearchIndexService pushes localized snapshots into Weaviate for search
|
||||
type SearchIndexService interface {
|
||||
IndexWork(ctx context.Context, work models.Work) error
|
||||
// IndexService pushes localized snapshots into Weaviate for search
|
||||
type IndexService interface {
|
||||
IndexWork(ctx context.Context, work domain.Work) error
|
||||
}
|
||||
|
||||
type searchIndexService struct {
|
||||
localization LocalizationService
|
||||
translations repositories.TranslationRepository
|
||||
type indexService struct {
|
||||
localization localization.Service
|
||||
translations domain.TranslationRepository
|
||||
}
|
||||
|
||||
func NewSearchIndexService(localization LocalizationService, translations repositories.TranslationRepository) SearchIndexService {
|
||||
return &searchIndexService{localization: localization, translations: translations}
|
||||
func NewIndexService(localization localization.Service, translations domain.TranslationRepository) IndexService {
|
||||
return &indexService{localization: localization, translations: translations}
|
||||
}
|
||||
|
||||
func (s *searchIndexService) IndexWork(ctx context.Context, work models.Work) error {
|
||||
func (s *indexService) IndexWork(ctx context.Context, work domain.Work) error {
|
||||
// Choose best content snapshot for indexing
|
||||
content, err := s.localization.GetWorkContent(ctx, work.ID, work.Language)
|
||||
if err != nil {
|
||||
1
internal/app/work/.keep
Normal file
1
internal/app/work/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
72
internal/app/work/commands.go
Normal file
72
internal/app/work/commands.go
Normal file
@ -0,0 +1,72 @@
|
||||
package work
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// WorkCommands contains the command handlers for the work aggregate.
|
||||
type WorkCommands struct {
|
||||
repo domain.WorkRepository
|
||||
analyzer interface { // This will be replaced with a proper interface later
|
||||
AnalyzeWork(ctx context.Context, workID uint) error
|
||||
}
|
||||
}
|
||||
|
||||
// NewWorkCommands creates a new WorkCommands handler.
|
||||
func NewWorkCommands(repo domain.WorkRepository, analyzer interface {
|
||||
AnalyzeWork(ctx context.Context, workID uint) error
|
||||
}) *WorkCommands {
|
||||
return &WorkCommands{
|
||||
repo: repo,
|
||||
analyzer: analyzer,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWork creates a new work.
|
||||
func (c *WorkCommands) CreateWork(ctx context.Context, work *domain.Work) error {
|
||||
if work == nil {
|
||||
return errors.New("work cannot be nil")
|
||||
}
|
||||
if work.Title == "" {
|
||||
return errors.New("work title cannot be empty")
|
||||
}
|
||||
if work.Language == "" {
|
||||
return errors.New("work language cannot be empty")
|
||||
}
|
||||
return c.repo.Create(ctx, work)
|
||||
}
|
||||
|
||||
// UpdateWork updates an existing work.
|
||||
func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error {
|
||||
if work == nil {
|
||||
return errors.New("work cannot be nil")
|
||||
}
|
||||
if work.ID == 0 {
|
||||
return errors.New("work ID cannot be zero")
|
||||
}
|
||||
if work.Title == "" {
|
||||
return errors.New("work title cannot be empty")
|
||||
}
|
||||
if work.Language == "" {
|
||||
return errors.New("work language cannot be empty")
|
||||
}
|
||||
return c.repo.Update(ctx, work)
|
||||
}
|
||||
|
||||
// DeleteWork deletes a work by ID.
|
||||
func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
||||
if id == 0 {
|
||||
return errors.New("invalid work ID")
|
||||
}
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// AnalyzeWork performs linguistic analysis on a work.
|
||||
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
|
||||
if workID == 0 {
|
||||
return errors.New("invalid work ID")
|
||||
}
|
||||
return c.analyzer.AnalyzeWork(ctx, workID)
|
||||
}
|
||||
94
internal/app/work/queries.go
Normal file
94
internal/app/work/queries.go
Normal file
@ -0,0 +1,94 @@
|
||||
package work
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// WorkAnalytics contains analytics data for a work
|
||||
type WorkAnalytics struct {
|
||||
WorkID uint
|
||||
ViewCount int64
|
||||
LikeCount int64
|
||||
CommentCount int64
|
||||
BookmarkCount int64
|
||||
TranslationCount int64
|
||||
ReadabilityScore float64
|
||||
SentimentScore float64
|
||||
TopKeywords []string
|
||||
PopularTranslations []TranslationAnalytics
|
||||
}
|
||||
|
||||
// TranslationAnalytics contains analytics data for a translation
|
||||
type TranslationAnalytics struct {
|
||||
TranslationID uint
|
||||
Language string
|
||||
ViewCount int64
|
||||
LikeCount int64
|
||||
}
|
||||
|
||||
// WorkQueries contains the query handlers for the work aggregate.
|
||||
type WorkQueries struct {
|
||||
repo domain.WorkRepository
|
||||
}
|
||||
|
||||
// NewWorkQueries creates a new WorkQueries handler.
|
||||
func NewWorkQueries(repo domain.WorkRepository) *WorkQueries {
|
||||
return &WorkQueries{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetWorkByID retrieves a work by ID.
|
||||
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
if id == 0 {
|
||||
return nil, errors.New("invalid work ID")
|
||||
}
|
||||
return q.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// ListWorks returns a paginated list of works.
|
||||
func (q *WorkQueries) ListWorks(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
return q.repo.List(ctx, page, pageSize)
|
||||
}
|
||||
|
||||
// GetWorkWithTranslations retrieves a work with its translations.
|
||||
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
if id == 0 {
|
||||
return nil, errors.New("invalid work ID")
|
||||
}
|
||||
return q.repo.GetWithTranslations(ctx, id)
|
||||
}
|
||||
|
||||
// FindWorksByTitle finds works by title.
|
||||
func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]domain.Work, error) {
|
||||
if title == "" {
|
||||
return nil, errors.New("title cannot be empty")
|
||||
}
|
||||
return q.repo.FindByTitle(ctx, title)
|
||||
}
|
||||
|
||||
// FindWorksByAuthor finds works by author ID.
|
||||
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
||||
if authorID == 0 {
|
||||
return nil, errors.New("invalid author ID")
|
||||
}
|
||||
return q.repo.FindByAuthor(ctx, authorID)
|
||||
}
|
||||
|
||||
// FindWorksByCategory finds works by category ID.
|
||||
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
|
||||
if categoryID == 0 {
|
||||
return nil, errors.New("invalid category ID")
|
||||
}
|
||||
return q.repo.FindByCategory(ctx, categoryID)
|
||||
}
|
||||
|
||||
// FindWorksByLanguage finds works by language.
|
||||
func (q *WorkQueries) FindWorksByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
if language == "" {
|
||||
return nil, errors.New("language cannot be empty")
|
||||
}
|
||||
return q.repo.FindByLanguage(ctx, language, page, pageSize)
|
||||
}
|
||||
1
internal/data/cache/.keep
vendored
Normal file
1
internal/data/cache/.keep
vendored
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1
internal/data/migrations/.keep
Normal file
1
internal/data/migrations/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1
internal/data/sql/.keep
Normal file
1
internal/data/sql/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
@ -1,35 +1,27 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// AuthorRepository defines CRUD methods specific to Author.
|
||||
type AuthorRepository interface {
|
||||
BaseRepository[models.Author]
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models.Author, error)
|
||||
ListByBookID(ctx context.Context, bookID uint) ([]models.Author, error)
|
||||
ListByCountryID(ctx context.Context, countryID uint) ([]models.Author, error)
|
||||
}
|
||||
|
||||
type authorRepository struct {
|
||||
BaseRepository[models.Author]
|
||||
domain.BaseRepository[domain.Author]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAuthorRepository creates a new AuthorRepository.
|
||||
func NewAuthorRepository(db *gorm.DB) AuthorRepository {
|
||||
func NewAuthorRepository(db *gorm.DB) domain.AuthorRepository {
|
||||
return &authorRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Author](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Author](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListByWorkID finds authors by work ID
|
||||
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Author, error) {
|
||||
var authors []models.Author
|
||||
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||
var authors []domain.Author
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id").
|
||||
Where("work_authors.work_id = ?", workID).
|
||||
Find(&authors).Error; err != nil {
|
||||
@ -39,8 +31,8 @@ func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]mod
|
||||
}
|
||||
|
||||
// ListByBookID finds authors by book ID
|
||||
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]models.Author, error) {
|
||||
var authors []models.Author
|
||||
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
||||
var authors []domain.Author
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id").
|
||||
Where("book_authors.book_id = ?", bookID).
|
||||
Find(&authors).Error; err != nil {
|
||||
@ -50,8 +42,8 @@ func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]mod
|
||||
}
|
||||
|
||||
// ListByCountryID finds authors by country ID
|
||||
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]models.Author, error) {
|
||||
var authors []models.Author
|
||||
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
||||
var authors []domain.Author
|
||||
if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1,14 +1,15 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/platform/log"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/platform/log"
|
||||
)
|
||||
|
||||
// Common repository errors
|
||||
@ -21,90 +22,13 @@ var (
|
||||
ErrTransactionFailed = errors.New("transaction failed")
|
||||
)
|
||||
|
||||
// PaginatedResult represents a paginated result set
|
||||
type PaginatedResult[T any] struct {
|
||||
Items []T `json:"items"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
HasPrev bool `json:"hasPrev"`
|
||||
}
|
||||
|
||||
// QueryOptions provides options for repository queries
|
||||
type QueryOptions struct {
|
||||
Preloads []string
|
||||
OrderBy string
|
||||
Where map[string]interface{}
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
// BaseRepository defines common CRUD operations that all repositories should implement
|
||||
type BaseRepository[T any] interface {
|
||||
// Create adds a new entity to the database
|
||||
Create(ctx context.Context, entity *T) error
|
||||
|
||||
// CreateInTx creates an entity within a transaction
|
||||
CreateInTx(ctx context.Context, tx *gorm.DB, entity *T) error
|
||||
|
||||
// GetByID retrieves an entity by its ID
|
||||
GetByID(ctx context.Context, id uint) (*T, error)
|
||||
|
||||
// GetByIDWithOptions retrieves an entity by its ID with query options
|
||||
GetByIDWithOptions(ctx context.Context, id uint, options *QueryOptions) (*T, error)
|
||||
|
||||
// Update updates an existing entity
|
||||
Update(ctx context.Context, entity *T) error
|
||||
|
||||
// UpdateInTx updates an entity within a transaction
|
||||
UpdateInTx(ctx context.Context, tx *gorm.DB, entity *T) error
|
||||
|
||||
// Delete removes an entity by its ID
|
||||
Delete(ctx context.Context, id uint) error
|
||||
|
||||
// DeleteInTx removes an entity by its ID within a transaction
|
||||
DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error
|
||||
|
||||
// List returns a paginated list of entities
|
||||
List(ctx context.Context, page, pageSize int) (*PaginatedResult[T], error)
|
||||
|
||||
// ListWithOptions returns entities with query options
|
||||
ListWithOptions(ctx context.Context, options *QueryOptions) ([]T, error)
|
||||
|
||||
// ListAll returns all entities (use with caution for large datasets)
|
||||
ListAll(ctx context.Context) ([]T, error)
|
||||
|
||||
// Count returns the total number of entities
|
||||
Count(ctx context.Context) (int64, error)
|
||||
|
||||
// CountWithOptions returns the count with query options
|
||||
CountWithOptions(ctx context.Context, options *QueryOptions) (int64, error)
|
||||
|
||||
// FindWithPreload retrieves an entity by its ID with preloaded relationships
|
||||
FindWithPreload(ctx context.Context, preloads []string, id uint) (*T, error)
|
||||
|
||||
// GetAllForSync returns entities in batches for synchronization
|
||||
GetAllForSync(ctx context.Context, batchSize, offset int) ([]T, error)
|
||||
|
||||
// Exists checks if an entity exists by ID
|
||||
Exists(ctx context.Context, id uint) (bool, error)
|
||||
|
||||
// BeginTx starts a new transaction
|
||||
BeginTx(ctx context.Context) (*gorm.DB, error)
|
||||
|
||||
// WithTx executes a function within a transaction
|
||||
WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error
|
||||
}
|
||||
|
||||
// BaseRepositoryImpl provides a default implementation of BaseRepository using GORM
|
||||
type BaseRepositoryImpl[T any] struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewBaseRepositoryImpl creates a new BaseRepositoryImpl
|
||||
func NewBaseRepositoryImpl[T any](db *gorm.DB) *BaseRepositoryImpl[T] {
|
||||
func NewBaseRepositoryImpl[T any](db *gorm.DB) domain.BaseRepository[T] {
|
||||
return &BaseRepositoryImpl[T]{db: db}
|
||||
}
|
||||
|
||||
@ -153,7 +77,7 @@ func (r *BaseRepositoryImpl[T]) validatePagination(page, pageSize int) (int, int
|
||||
}
|
||||
|
||||
// buildQuery applies query options to a GORM query
|
||||
func (r *BaseRepositoryImpl[T]) buildQuery(query *gorm.DB, options *QueryOptions) *gorm.DB {
|
||||
func (r *BaseRepositoryImpl[T]) buildQuery(query *gorm.DB, options *domain.QueryOptions) *gorm.DB {
|
||||
if options == nil {
|
||||
return query
|
||||
}
|
||||
@ -272,7 +196,7 @@ func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uint) (*T, error
|
||||
}
|
||||
|
||||
// GetByIDWithOptions retrieves an entity by its ID with query options
|
||||
func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uint, options *QueryOptions) (*T, error) {
|
||||
func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*T, error) {
|
||||
if err := r.validateContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -435,7 +359,7 @@ func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id
|
||||
}
|
||||
|
||||
// List returns a paginated list of entities
|
||||
func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*PaginatedResult[T], error) {
|
||||
func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[T], error) {
|
||||
if err := r.validateContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -490,7 +414,7 @@ func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*
|
||||
log.F("hasPrev", hasPrev),
|
||||
log.F("duration", duration))
|
||||
|
||||
return &PaginatedResult[T]{
|
||||
return &domain.PaginatedResult[T]{
|
||||
Items: entities,
|
||||
TotalCount: totalCount,
|
||||
Page: page,
|
||||
@ -502,7 +426,7 @@ func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*
|
||||
}
|
||||
|
||||
// ListWithOptions returns entities with query options
|
||||
func (r *BaseRepositoryImpl[T]) ListWithOptions(ctx context.Context, options *QueryOptions) ([]T, error) {
|
||||
func (r *BaseRepositoryImpl[T]) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]T, error) {
|
||||
if err := r.validateContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -573,7 +497,7 @@ func (r *BaseRepositoryImpl[T]) Count(ctx context.Context) (int64, error) {
|
||||
}
|
||||
|
||||
// CountWithOptions returns the count with query options
|
||||
func (r *BaseRepositoryImpl[T]) CountWithOptions(ctx context.Context, options *QueryOptions) (int64, error) {
|
||||
func (r *BaseRepositoryImpl[T]) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
if err := r.validateContext(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -1,37 +1,28 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// BookRepository defines CRUD methods specific to Book.
|
||||
type BookRepository interface {
|
||||
BaseRepository[models.Book]
|
||||
ListByAuthorID(ctx context.Context, authorID uint) ([]models.Book, error)
|
||||
ListByPublisherID(ctx context.Context, publisherID uint) ([]models.Book, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models.Book, error)
|
||||
FindByISBN(ctx context.Context, isbn string) (*models.Book, error)
|
||||
}
|
||||
|
||||
type bookRepository struct {
|
||||
BaseRepository[models.Book]
|
||||
domain.BaseRepository[domain.Book]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewBookRepository creates a new BookRepository.
|
||||
func NewBookRepository(db *gorm.DB) BookRepository {
|
||||
func NewBookRepository(db *gorm.DB) domain.BookRepository {
|
||||
return &bookRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Book](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Book](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListByAuthorID finds books by author ID
|
||||
func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]models.Book, error) {
|
||||
var books []models.Book
|
||||
func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]domain.Book, error) {
|
||||
var books []domain.Book
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.book_id = books.id").
|
||||
Where("book_authors.author_id = ?", authorID).
|
||||
Find(&books).Error; err != nil {
|
||||
@ -41,8 +32,8 @@ func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]m
|
||||
}
|
||||
|
||||
// ListByPublisherID finds books by publisher ID
|
||||
func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint) ([]models.Book, error) {
|
||||
var books []models.Book
|
||||
func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint) ([]domain.Book, error) {
|
||||
var books []domain.Book
|
||||
if err := r.db.WithContext(ctx).Where("publisher_id = ?", publisherID).Find(&books).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -50,8 +41,8 @@ func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint
|
||||
}
|
||||
|
||||
// ListByWorkID finds books by work ID
|
||||
func (r *bookRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Book, error) {
|
||||
var books []models.Book
|
||||
func (r *bookRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Book, error) {
|
||||
var books []domain.Book
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN book_works ON book_works.book_id = books.id").
|
||||
Where("book_works.work_id = ?", workID).
|
||||
Find(&books).Error; err != nil {
|
||||
@ -61,8 +52,8 @@ func (r *bookRepository) ListByWorkID(ctx context.Context, workID uint) ([]model
|
||||
}
|
||||
|
||||
// FindByISBN finds a book by ISBN
|
||||
func (r *bookRepository) FindByISBN(ctx context.Context, isbn string) (*models.Book, error) {
|
||||
var book models.Book
|
||||
func (r *bookRepository) FindByISBN(ctx context.Context, isbn string) (*domain.Book, error) {
|
||||
var book domain.Book
|
||||
if err := r.db.WithContext(ctx).Where("isbn = ?", isbn).First(&book).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
@ -1,34 +1,27 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// BookmarkRepository defines CRUD methods specific to Bookmark.
|
||||
type BookmarkRepository interface {
|
||||
BaseRepository[models.Bookmark]
|
||||
ListByUserID(ctx context.Context, userID uint) ([]models.Bookmark, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models.Bookmark, error)
|
||||
}
|
||||
|
||||
type bookmarkRepository struct {
|
||||
BaseRepository[models.Bookmark]
|
||||
domain.BaseRepository[domain.Bookmark]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewBookmarkRepository creates a new BookmarkRepository.
|
||||
func NewBookmarkRepository(db *gorm.DB) BookmarkRepository {
|
||||
func NewBookmarkRepository(db *gorm.DB) domain.BookmarkRepository {
|
||||
return &bookmarkRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Bookmark](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListByUserID finds bookmarks by user ID
|
||||
func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]models.Bookmark, error) {
|
||||
var bookmarks []models.Bookmark
|
||||
func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error) {
|
||||
var bookmarks []domain.Bookmark
|
||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&bookmarks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -36,8 +29,8 @@ func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]m
|
||||
}
|
||||
|
||||
// ListByWorkID finds bookmarks by work ID
|
||||
func (r *bookmarkRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Bookmark, error) {
|
||||
var bookmarks []models.Bookmark
|
||||
func (r *bookmarkRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) {
|
||||
var bookmarks []domain.Bookmark
|
||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&bookmarks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1,36 +1,28 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// CategoryRepository defines CRUD methods specific to Category.
|
||||
type CategoryRepository interface {
|
||||
BaseRepository[models.Category]
|
||||
FindByName(ctx context.Context, name string) (*models.Category, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models.Category, error)
|
||||
ListByParentID(ctx context.Context, parentID *uint) ([]models.Category, error)
|
||||
}
|
||||
|
||||
type categoryRepository struct {
|
||||
BaseRepository[models.Category]
|
||||
domain.BaseRepository[domain.Category]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCategoryRepository creates a new CategoryRepository.
|
||||
func NewCategoryRepository(db *gorm.DB) CategoryRepository {
|
||||
func NewCategoryRepository(db *gorm.DB) domain.CategoryRepository {
|
||||
return &categoryRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Category](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Category](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// FindByName finds a category by name
|
||||
func (r *categoryRepository) FindByName(ctx context.Context, name string) (*models.Category, error) {
|
||||
var category models.Category
|
||||
func (r *categoryRepository) FindByName(ctx context.Context, name string) (*domain.Category, error) {
|
||||
var category domain.Category
|
||||
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&category).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
@ -41,8 +33,8 @@ func (r *categoryRepository) FindByName(ctx context.Context, name string) (*mode
|
||||
}
|
||||
|
||||
// ListByWorkID finds categories by work ID
|
||||
func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Category, error) {
|
||||
var categories []models.Category
|
||||
func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) {
|
||||
var categories []domain.Category
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN work_categories ON work_categories.category_id = categories.id").
|
||||
Where("work_categories.work_id = ?", workID).
|
||||
Find(&categories).Error; err != nil {
|
||||
@ -52,8 +44,8 @@ func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]m
|
||||
}
|
||||
|
||||
// ListByParentID finds categories by parent ID
|
||||
func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uint) ([]models.Category, error) {
|
||||
var categories []models.Category
|
||||
func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) {
|
||||
var categories []domain.Category
|
||||
if parentID == nil {
|
||||
if err := r.db.WithContext(ctx).Where("parent_id IS NULL").Find(&categories).Error; err != nil {
|
||||
return nil, err
|
||||
@ -1,35 +1,27 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// CollectionRepository defines CRUD methods specific to Collection.
|
||||
type CollectionRepository interface {
|
||||
BaseRepository[models.Collection]
|
||||
ListByUserID(ctx context.Context, userID uint) ([]models.Collection, error)
|
||||
ListPublic(ctx context.Context) ([]models.Collection, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models.Collection, error)
|
||||
}
|
||||
|
||||
type collectionRepository struct {
|
||||
BaseRepository[models.Collection]
|
||||
domain.BaseRepository[domain.Collection]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCollectionRepository creates a new CollectionRepository.
|
||||
func NewCollectionRepository(db *gorm.DB) CollectionRepository {
|
||||
func NewCollectionRepository(db *gorm.DB) domain.CollectionRepository {
|
||||
return &collectionRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Collection](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListByUserID finds collections by user ID
|
||||
func (r *collectionRepository) ListByUserID(ctx context.Context, userID uint) ([]models.Collection, error) {
|
||||
var collections []models.Collection
|
||||
func (r *collectionRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Collection, error) {
|
||||
var collections []domain.Collection
|
||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&collections).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -37,8 +29,8 @@ func (r *collectionRepository) ListByUserID(ctx context.Context, userID uint) ([
|
||||
}
|
||||
|
||||
// ListPublic finds public collections
|
||||
func (r *collectionRepository) ListPublic(ctx context.Context) ([]models.Collection, error) {
|
||||
var collections []models.Collection
|
||||
func (r *collectionRepository) ListPublic(ctx context.Context) ([]domain.Collection, error) {
|
||||
var collections []domain.Collection
|
||||
if err := r.db.WithContext(ctx).Where("is_public = ?", true).Find(&collections).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -46,8 +38,8 @@ func (r *collectionRepository) ListPublic(ctx context.Context) ([]models.Collect
|
||||
}
|
||||
|
||||
// ListByWorkID finds collections by work ID
|
||||
func (r *collectionRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Collection, error) {
|
||||
var collections []models.Collection
|
||||
func (r *collectionRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) {
|
||||
var collections []domain.Collection
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN collection_works ON collection_works.collection_id = collections.id").
|
||||
Where("collection_works.work_id = ?", workID).
|
||||
Find(&collections).Error; err != nil {
|
||||
@ -1,36 +1,27 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// CommentRepository defines CRUD methods specific to Comment.
|
||||
type CommentRepository interface {
|
||||
BaseRepository[models.Comment]
|
||||
ListByUserID(ctx context.Context, userID uint) ([]models.Comment, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models.Comment, error)
|
||||
ListByTranslationID(ctx context.Context, translationID uint) ([]models.Comment, error)
|
||||
ListByParentID(ctx context.Context, parentID uint) ([]models.Comment, error)
|
||||
}
|
||||
|
||||
type commentRepository struct {
|
||||
BaseRepository[models.Comment]
|
||||
domain.BaseRepository[domain.Comment]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCommentRepository creates a new CommentRepository.
|
||||
func NewCommentRepository(db *gorm.DB) CommentRepository {
|
||||
func NewCommentRepository(db *gorm.DB) domain.CommentRepository {
|
||||
return &commentRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Comment](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListByUserID finds comments by user ID
|
||||
func (r *commentRepository) ListByUserID(ctx context.Context, userID uint) ([]models.Comment, error) {
|
||||
var comments []models.Comment
|
||||
func (r *commentRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Comment, error) {
|
||||
var comments []domain.Comment
|
||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&comments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -38,8 +29,8 @@ func (r *commentRepository) ListByUserID(ctx context.Context, userID uint) ([]mo
|
||||
}
|
||||
|
||||
// ListByWorkID finds comments by work ID
|
||||
func (r *commentRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Comment, error) {
|
||||
var comments []models.Comment
|
||||
func (r *commentRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Comment, error) {
|
||||
var comments []domain.Comment
|
||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&comments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -47,8 +38,8 @@ func (r *commentRepository) ListByWorkID(ctx context.Context, workID uint) ([]mo
|
||||
}
|
||||
|
||||
// ListByTranslationID finds comments by translation ID
|
||||
func (r *commentRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]models.Comment, error) {
|
||||
var comments []models.Comment
|
||||
func (r *commentRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Comment, error) {
|
||||
var comments []domain.Comment
|
||||
if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&comments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -56,8 +47,8 @@ func (r *commentRepository) ListByTranslationID(ctx context.Context, translation
|
||||
}
|
||||
|
||||
// ListByParentID finds comments by parent ID
|
||||
func (r *commentRepository) ListByParentID(ctx context.Context, parentID uint) ([]models.Comment, error) {
|
||||
var comments []models.Comment
|
||||
func (r *commentRepository) ListByParentID(ctx context.Context, parentID uint) ([]domain.Comment, error) {
|
||||
var comments []domain.Comment
|
||||
if err := r.db.WithContext(ctx).Where("parent_id = ?", parentID).Find(&comments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
93
internal/data/sql/copyright_repository.go
Normal file
93
internal/data/sql/copyright_repository.go
Normal file
@ -0,0 +1,93 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
)
|
||||
|
||||
// CopyrightRepository defines CRUD methods specific to Copyright.
|
||||
type CopyrightRepository interface {
|
||||
BaseRepository[models.Copyright]
|
||||
// Polymorphic methods
|
||||
AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error
|
||||
DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error
|
||||
GetByEntity(ctx context.Context, entityID uint, entityType string) ([]models.Copyright, error)
|
||||
GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]models.Copyrightable, error)
|
||||
// Translation methods
|
||||
AddTranslation(ctx context.Context, translation *models.CopyrightTranslation) error
|
||||
GetTranslations(ctx context.Context, copyrightID uint) ([]models.CopyrightTranslation, error)
|
||||
GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*models.CopyrightTranslation, error)
|
||||
}
|
||||
|
||||
type copyrightRepository struct {
|
||||
BaseRepository[models.Copyright]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCopyrightRepository creates a new CopyrightRepository.
|
||||
func NewCopyrightRepository(db *gorm.DB) CopyrightRepository {
|
||||
return ©rightRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Copyright](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// AttachToEntity attaches a copyright to any entity type
|
||||
func (r *copyrightRepository) AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error {
|
||||
copyrightable := models.Copyrightable{
|
||||
CopyrightID: copyrightID,
|
||||
CopyrightableID: entityID,
|
||||
CopyrightableType: entityType,
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(©rightable).Error
|
||||
}
|
||||
|
||||
// DetachFromEntity removes a copyright from an entity
|
||||
func (r *copyrightRepository) DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error {
|
||||
return r.db.WithContext(ctx).Where("copyright_id = ? AND copyrightable_id = ? AND copyrightable_type = ?",
|
||||
copyrightID, entityID, entityType).Delete(&models.Copyrightable{}).Error
|
||||
}
|
||||
|
||||
// GetByEntity gets all copyrights for a specific entity
|
||||
func (r *copyrightRepository) GetByEntity(ctx context.Context, entityID uint, entityType string) ([]models.Copyright, error) {
|
||||
var copyrights []models.Copyright
|
||||
err := r.db.WithContext(ctx).Joins("JOIN copyrightables ON copyrightables.copyright_id = copyrights.id").
|
||||
Where("copyrightables.copyrightable_id = ? AND copyrightables.copyrightable_type = ?", entityID, entityType).
|
||||
Preload("Translations").
|
||||
Find(©rights).Error
|
||||
return copyrights, err
|
||||
}
|
||||
|
||||
// GetEntitiesByCopyright gets all entities that have a specific copyright
|
||||
func (r *copyrightRepository) GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]models.Copyrightable, error) {
|
||||
var copyrightables []models.Copyrightable
|
||||
err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(©rightables).Error
|
||||
return copyrightables, err
|
||||
}
|
||||
|
||||
// AddTranslation adds a translation to a copyright
|
||||
func (r *copyrightRepository) AddTranslation(ctx context.Context, translation *models.CopyrightTranslation) error {
|
||||
return r.db.WithContext(ctx).Create(translation).Error
|
||||
}
|
||||
|
||||
// GetTranslations gets all translations for a copyright
|
||||
func (r *copyrightRepository) GetTranslations(ctx context.Context, copyrightID uint) ([]models.CopyrightTranslation, error) {
|
||||
var translations []models.CopyrightTranslation
|
||||
err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(&translations).Error
|
||||
return translations, err
|
||||
}
|
||||
|
||||
// GetTranslationByLanguage gets a specific translation by language code
|
||||
func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*models.CopyrightTranslation, error) {
|
||||
var translation models.CopyrightTranslation
|
||||
err := r.db.WithContext(ctx).Where("copyright_id = ? AND language_code = ?", copyrightID, languageCode).First(&translation).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &translation, nil
|
||||
}
|
||||
@ -1,36 +1,27 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// LikeRepository defines CRUD methods specific to Like.
|
||||
type LikeRepository interface {
|
||||
BaseRepository[models.Like]
|
||||
ListByUserID(ctx context.Context, userID uint) ([]models.Like, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models.Like, error)
|
||||
ListByTranslationID(ctx context.Context, translationID uint) ([]models.Like, error)
|
||||
ListByCommentID(ctx context.Context, commentID uint) ([]models.Like, error)
|
||||
}
|
||||
|
||||
type likeRepository struct {
|
||||
BaseRepository[models.Like]
|
||||
domain.BaseRepository[domain.Like]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewLikeRepository creates a new LikeRepository.
|
||||
func NewLikeRepository(db *gorm.DB) LikeRepository {
|
||||
func NewLikeRepository(db *gorm.DB) domain.LikeRepository {
|
||||
return &likeRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Like](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Like](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListByUserID finds likes by user ID
|
||||
func (r *likeRepository) ListByUserID(ctx context.Context, userID uint) ([]models.Like, error) {
|
||||
var likes []models.Like
|
||||
func (r *likeRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Like, error) {
|
||||
var likes []domain.Like
|
||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&likes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -38,8 +29,8 @@ func (r *likeRepository) ListByUserID(ctx context.Context, userID uint) ([]model
|
||||
}
|
||||
|
||||
// ListByWorkID finds likes by work ID
|
||||
func (r *likeRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Like, error) {
|
||||
var likes []models.Like
|
||||
func (r *likeRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Like, error) {
|
||||
var likes []domain.Like
|
||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&likes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -47,8 +38,8 @@ func (r *likeRepository) ListByWorkID(ctx context.Context, workID uint) ([]model
|
||||
}
|
||||
|
||||
// ListByTranslationID finds likes by translation ID
|
||||
func (r *likeRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]models.Like, error) {
|
||||
var likes []models.Like
|
||||
func (r *likeRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Like, error) {
|
||||
var likes []domain.Like
|
||||
if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&likes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -56,8 +47,8 @@ func (r *likeRepository) ListByTranslationID(ctx context.Context, translationID
|
||||
}
|
||||
|
||||
// ListByCommentID finds likes by comment ID
|
||||
func (r *likeRepository) ListByCommentID(ctx context.Context, commentID uint) ([]models.Like, error) {
|
||||
var likes []models.Like
|
||||
func (r *likeRepository) ListByCommentID(ctx context.Context, commentID uint) ([]domain.Like, error) {
|
||||
var likes []domain.Like
|
||||
if err := r.db.WithContext(ctx).Where("comment_id = ?", commentID).Find(&likes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1,35 +1,28 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// TagRepository defines CRUD methods specific to Tag.
|
||||
type TagRepository interface {
|
||||
BaseRepository[models.Tag]
|
||||
FindByName(ctx context.Context, name string) (*models.Tag, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models.Tag, error)
|
||||
}
|
||||
|
||||
type tagRepository struct {
|
||||
BaseRepository[models.Tag]
|
||||
domain.BaseRepository[domain.Tag]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTagRepository creates a new TagRepository.
|
||||
func NewTagRepository(db *gorm.DB) TagRepository {
|
||||
func NewTagRepository(db *gorm.DB) domain.TagRepository {
|
||||
return &tagRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Tag](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// FindByName finds a tag by name
|
||||
func (r *tagRepository) FindByName(ctx context.Context, name string) (*models.Tag, error) {
|
||||
var tag models.Tag
|
||||
func (r *tagRepository) FindByName(ctx context.Context, name string) (*domain.Tag, error) {
|
||||
var tag domain.Tag
|
||||
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&tag).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
@ -40,8 +33,8 @@ func (r *tagRepository) FindByName(ctx context.Context, name string) (*models.Ta
|
||||
}
|
||||
|
||||
// ListByWorkID finds tags by work ID
|
||||
func (r *tagRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Tag, error) {
|
||||
var tags []models.Tag
|
||||
func (r *tagRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) {
|
||||
var tags []domain.Tag
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN work_tags ON work_tags.tag_id = tags.id").
|
||||
Where("work_tags.work_id = ?", workID).
|
||||
Find(&tags).Error; err != nil {
|
||||
@ -1,36 +1,27 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
models2 "tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// TranslationRepository defines CRUD methods specific to Translation.
|
||||
type TranslationRepository interface {
|
||||
BaseRepository[models2.Translation]
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]models2.Translation, error)
|
||||
ListByEntity(ctx context.Context, entityType string, entityID uint) ([]models2.Translation, error)
|
||||
ListByTranslatorID(ctx context.Context, translatorID uint) ([]models2.Translation, error)
|
||||
ListByStatus(ctx context.Context, status models2.TranslationStatus) ([]models2.Translation, error)
|
||||
}
|
||||
|
||||
type translationRepository struct {
|
||||
BaseRepository[models2.Translation]
|
||||
domain.BaseRepository[domain.Translation]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTranslationRepository creates a new TranslationRepository.
|
||||
func NewTranslationRepository(db *gorm.DB) TranslationRepository {
|
||||
func NewTranslationRepository(db *gorm.DB) domain.TranslationRepository {
|
||||
return &translationRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models2.Translation](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Translation](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListByWorkID finds translations by work ID
|
||||
func (r *translationRepository) ListByWorkID(ctx context.Context, workID uint) ([]models2.Translation, error) {
|
||||
var translations []models2.Translation
|
||||
func (r *translationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||
var translations []domain.Translation
|
||||
if err := r.db.WithContext(ctx).Where("translatable_id = ? AND translatable_type = ?", workID, "Work").Find(&translations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -38,8 +29,8 @@ func (r *translationRepository) ListByWorkID(ctx context.Context, workID uint) (
|
||||
}
|
||||
|
||||
// ListByEntity finds translations by entity type and ID
|
||||
func (r *translationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]models2.Translation, error) {
|
||||
var translations []models2.Translation
|
||||
func (r *translationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||
var translations []domain.Translation
|
||||
if err := r.db.WithContext(ctx).Where("translatable_id = ? AND translatable_type = ?", entityID, entityType).Find(&translations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -47,8 +38,8 @@ func (r *translationRepository) ListByEntity(ctx context.Context, entityType str
|
||||
}
|
||||
|
||||
// ListByTranslatorID finds translations by translator ID
|
||||
func (r *translationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]models2.Translation, error) {
|
||||
var translations []models2.Translation
|
||||
func (r *translationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
||||
var translations []domain.Translation
|
||||
if err := r.db.WithContext(ctx).Where("translator_id = ?", translatorID).Find(&translations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -56,8 +47,8 @@ func (r *translationRepository) ListByTranslatorID(ctx context.Context, translat
|
||||
}
|
||||
|
||||
// ListByStatus finds translations by status
|
||||
func (r *translationRepository) ListByStatus(ctx context.Context, status models2.TranslationStatus) ([]models2.Translation, error) {
|
||||
var translations []models2.Translation
|
||||
func (r *translationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
||||
var translations []domain.Translation
|
||||
if err := r.db.WithContext(ctx).Where("status = ?", status).Find(&translations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1,36 +1,28 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
models2 "tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// UserRepository defines CRUD methods specific to User.
|
||||
type UserRepository interface {
|
||||
BaseRepository[models2.User]
|
||||
FindByUsername(ctx context.Context, username string) (*models2.User, error)
|
||||
FindByEmail(ctx context.Context, email string) (*models2.User, error)
|
||||
ListByRole(ctx context.Context, role models2.UserRole) ([]models2.User, error)
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
BaseRepository[models2.User]
|
||||
domain.BaseRepository[domain.User]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository creates a new UserRepository.
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
func NewUserRepository(db *gorm.DB) domain.UserRepository {
|
||||
return &userRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models2.User](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.User](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// FindByUsername finds a user by username
|
||||
func (r *userRepository) FindByUsername(ctx context.Context, username string) (*models2.User, error) {
|
||||
var user models2.User
|
||||
func (r *userRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
if err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
@ -41,8 +33,8 @@ func (r *userRepository) FindByUsername(ctx context.Context, username string) (*
|
||||
}
|
||||
|
||||
// FindByEmail finds a user by email
|
||||
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models2.User, error) {
|
||||
var user models2.User
|
||||
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
if err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
@ -53,8 +45,8 @@ func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models
|
||||
}
|
||||
|
||||
// ListByRole lists users by role
|
||||
func (r *userRepository) ListByRole(ctx context.Context, role models2.UserRole) ([]models2.User, error) {
|
||||
var users []models2.User
|
||||
func (r *userRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
if err := r.db.WithContext(ctx).Where("role = ?", role).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1,38 +1,27 @@
|
||||
package repositories
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/models"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
// WorkRepository defines methods specific to Work.
|
||||
type WorkRepository interface {
|
||||
BaseRepository[models.Work]
|
||||
FindByTitle(ctx context.Context, title string) ([]models.Work, error)
|
||||
FindByAuthor(ctx context.Context, authorID uint) ([]models.Work, error)
|
||||
FindByCategory(ctx context.Context, categoryID uint) ([]models.Work, error)
|
||||
FindByLanguage(ctx context.Context, language string, page, pageSize int) (*PaginatedResult[models.Work], error)
|
||||
GetWithTranslations(ctx context.Context, id uint) (*models.Work, error)
|
||||
ListWithTranslations(ctx context.Context, page, pageSize int) (*PaginatedResult[models.Work], error)
|
||||
}
|
||||
|
||||
type workRepository struct {
|
||||
BaseRepository[models.Work]
|
||||
domain.BaseRepository[domain.Work]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewWorkRepository creates a new WorkRepository.
|
||||
func NewWorkRepository(db *gorm.DB) WorkRepository {
|
||||
func NewWorkRepository(db *gorm.DB) domain.WorkRepository {
|
||||
return &workRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[models.Work](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Work](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// FindByTitle finds works by title (partial match)
|
||||
func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]models.Work, error) {
|
||||
var works []models.Work
|
||||
func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
|
||||
var works []domain.Work
|
||||
if err := r.db.WithContext(ctx).Where("title LIKE ?", "%"+title+"%").Find(&works).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -40,8 +29,8 @@ func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]model
|
||||
}
|
||||
|
||||
// FindByAuthor finds works by author ID
|
||||
func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]models.Work, error) {
|
||||
var works []models.Work
|
||||
func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
||||
var works []domain.Work
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.work_id = works.id").
|
||||
Where("work_authors.author_id = ?", authorID).
|
||||
Find(&works).Error; err != nil {
|
||||
@ -51,8 +40,8 @@ func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]mod
|
||||
}
|
||||
|
||||
// FindByCategory finds works by category ID
|
||||
func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([]models.Work, error) {
|
||||
var works []models.Work
|
||||
func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
|
||||
var works []domain.Work
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN work_categories ON work_categories.work_id = works.id").
|
||||
Where("work_categories.category_id = ?", categoryID).
|
||||
Find(&works).Error; err != nil {
|
||||
@ -62,7 +51,7 @@ func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([
|
||||
}
|
||||
|
||||
// FindByLanguage finds works by language with pagination
|
||||
func (r *workRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*PaginatedResult[models.Work], error) {
|
||||
func (r *workRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
@ -71,11 +60,11 @@ func (r *workRepository) FindByLanguage(ctx context.Context, language string, pa
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
var works []models.Work
|
||||
var works []domain.Work
|
||||
var totalCount int64
|
||||
|
||||
// Get total count
|
||||
if err := r.db.WithContext(ctx).Model(&models.Work{}).Where("language = ?", language).Count(&totalCount).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&domain.Work{}).Where("language = ?", language).Count(&totalCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -98,7 +87,7 @@ func (r *workRepository) FindByLanguage(ctx context.Context, language string, pa
|
||||
hasNext := page < totalPages
|
||||
hasPrev := page > 1
|
||||
|
||||
return &PaginatedResult[models.Work]{
|
||||
return &domain.PaginatedResult[domain.Work]{
|
||||
Items: works,
|
||||
TotalCount: totalCount,
|
||||
Page: page,
|
||||
@ -110,12 +99,12 @@ func (r *workRepository) FindByLanguage(ctx context.Context, language string, pa
|
||||
}
|
||||
|
||||
// GetWithTranslations gets a work with its translations
|
||||
func (r *workRepository) GetWithTranslations(ctx context.Context, id uint) (*models.Work, error) {
|
||||
func (r *workRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
return r.FindWithPreload(ctx, []string{"Translations"}, id)
|
||||
}
|
||||
|
||||
// ListWithTranslations lists works with their translations
|
||||
func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*PaginatedResult[models.Work], error) {
|
||||
func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
@ -124,11 +113,11 @@ func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSiz
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
var works []models.Work
|
||||
var works []domain.Work
|
||||
var totalCount int64
|
||||
|
||||
// Get total count
|
||||
if err := r.db.WithContext(ctx).Model(&models.Work{}).Count(&totalCount).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&domain.Work{}).Count(&totalCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -151,7 +140,7 @@ func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSiz
|
||||
hasNext := page < totalPages
|
||||
hasPrev := page > 1
|
||||
|
||||
return &PaginatedResult[models.Work]{
|
||||
return &domain.PaginatedResult[domain.Work]{
|
||||
Items: works,
|
||||
TotalCount: totalCount,
|
||||
Page: page,
|
||||
1
internal/domain/.keep
Normal file
1
internal/domain/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1053
internal/domain/entities.go
Normal file
1053
internal/domain/entities.go
Normal file
File diff suppressed because it is too large
Load Diff
153
internal/domain/interfaces.go
Normal file
153
internal/domain/interfaces.go
Normal file
@ -0,0 +1,153 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PaginatedResult represents a paginated result set
|
||||
type PaginatedResult[T any] struct {
|
||||
Items []T `json:"items"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
HasPrev bool `json:"hasPrev"`
|
||||
}
|
||||
|
||||
// QueryOptions provides options for repository queries
|
||||
type QueryOptions struct {
|
||||
Preloads []string
|
||||
OrderBy string
|
||||
Where map[string]interface{}
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
// BaseRepository defines common CRUD operations that all repositories should implement
|
||||
type BaseRepository[T any] interface {
|
||||
Create(ctx context.Context, entity *T) error
|
||||
CreateInTx(ctx context.Context, tx *gorm.DB, entity *T) error
|
||||
GetByID(ctx context.Context, id uint) (*T, error)
|
||||
GetByIDWithOptions(ctx context.Context, id uint, options *QueryOptions) (*T, error)
|
||||
Update(ctx context.Context, entity *T) error
|
||||
UpdateInTx(ctx context.Context, tx *gorm.DB, entity *T) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error
|
||||
List(ctx context.Context, page, pageSize int) (*PaginatedResult[T], error)
|
||||
ListWithOptions(ctx context.Context, options *QueryOptions) ([]T, error)
|
||||
ListAll(ctx context.Context) ([]T, error)
|
||||
Count(ctx context.Context) (int64, error)
|
||||
CountWithOptions(ctx context.Context, options *QueryOptions) (int64, error)
|
||||
FindWithPreload(ctx context.Context, preloads []string, id uint) (*T, error)
|
||||
GetAllForSync(ctx context.Context, batchSize, offset int) ([]T, error)
|
||||
Exists(ctx context.Context, id uint) (bool, error)
|
||||
BeginTx(ctx context.Context) (*gorm.DB, error)
|
||||
WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error
|
||||
}
|
||||
|
||||
// WorkRepository defines methods specific to Work.
|
||||
type WorkRepository interface {
|
||||
BaseRepository[Work]
|
||||
FindByTitle(ctx context.Context, title string) ([]Work, error)
|
||||
FindByAuthor(ctx context.Context, authorID uint) ([]Work, error)
|
||||
FindByCategory(ctx context.Context, categoryID uint) ([]Work, error)
|
||||
FindByLanguage(ctx context.Context, language string, page, pageSize int) (*PaginatedResult[Work], error)
|
||||
GetWithTranslations(ctx context.Context, id uint) (*Work, error)
|
||||
ListWithTranslations(ctx context.Context, page, pageSize int) (*PaginatedResult[Work], error)
|
||||
}
|
||||
|
||||
// AuthorRepository defines CRUD methods specific to Author.
|
||||
type AuthorRepository interface {
|
||||
BaseRepository[Author]
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Author, error)
|
||||
ListByBookID(ctx context.Context, bookID uint) ([]Author, error)
|
||||
ListByCountryID(ctx context.Context, countryID uint) ([]Author, error)
|
||||
}
|
||||
|
||||
// BookRepository defines CRUD methods specific to Book.
|
||||
type BookRepository interface {
|
||||
BaseRepository[Book]
|
||||
ListByAuthorID(ctx context.Context, authorID uint) ([]Book, error)
|
||||
ListByPublisherID(ctx context.Context, publisherID uint) ([]Book, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Book, error)
|
||||
FindByISBN(ctx context.Context, isbn string) (*Book, error)
|
||||
}
|
||||
|
||||
// UserRepository defines CRUD methods specific to User.
|
||||
type UserRepository interface {
|
||||
BaseRepository[User]
|
||||
FindByUsername(ctx context.Context, username string) (*User, error)
|
||||
FindByEmail(ctx context.Context, email string) (*User, error)
|
||||
ListByRole(ctx context.Context, role UserRole) ([]User, error)
|
||||
}
|
||||
|
||||
// TranslationRepository defines CRUD methods specific to Translation.
|
||||
type TranslationRepository interface {
|
||||
BaseRepository[Translation]
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Translation, error)
|
||||
ListByEntity(ctx context.Context, entityType string, entityID uint) ([]Translation, error)
|
||||
ListByTranslatorID(ctx context.Context, translatorID uint) ([]Translation, error)
|
||||
ListByStatus(ctx context.Context, status TranslationStatus) ([]Translation, error)
|
||||
}
|
||||
|
||||
// CommentRepository defines CRUD methods specific to Comment.
|
||||
type CommentRepository interface {
|
||||
BaseRepository[Comment]
|
||||
ListByUserID(ctx context.Context, userID uint) ([]Comment, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Comment, error)
|
||||
ListByTranslationID(ctx context.Context, translationID uint) ([]Comment, error)
|
||||
ListByParentID(ctx context.Context, parentID uint) ([]Comment, error)
|
||||
}
|
||||
|
||||
// LikeRepository defines CRUD methods specific to Like.
|
||||
type LikeRepository interface {
|
||||
BaseRepository[Like]
|
||||
ListByUserID(ctx context.Context, userID uint) ([]Like, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Like, error)
|
||||
ListByTranslationID(ctx context.Context, translationID uint) ([]Like, error)
|
||||
ListByCommentID(ctx context.Context, commentID uint) ([]Like, error)
|
||||
}
|
||||
|
||||
// BookmarkRepository defines CRUD methods specific to Bookmark.
|
||||
type BookmarkRepository interface {
|
||||
BaseRepository[Bookmark]
|
||||
ListByUserID(ctx context.Context, userID uint) ([]Bookmark, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Bookmark, error)
|
||||
}
|
||||
|
||||
// CollectionRepository defines CRUD methods specific to Collection.
|
||||
type CollectionRepository interface {
|
||||
BaseRepository[Collection]
|
||||
ListByUserID(ctx context.Context, userID uint) ([]Collection, error)
|
||||
ListPublic(ctx context.Context) ([]Collection, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Collection, error)
|
||||
}
|
||||
|
||||
// TagRepository defines CRUD methods specific to Tag.
|
||||
type TagRepository interface {
|
||||
BaseRepository[Tag]
|
||||
FindByName(ctx context.Context, name string) (*Tag, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Tag, error)
|
||||
}
|
||||
|
||||
// CategoryRepository defines CRUD methods specific to Category.
|
||||
type CategoryRepository interface {
|
||||
BaseRepository[Category]
|
||||
FindByName(ctx context.Context, name string) (*Category, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Category, error)
|
||||
ListByParentID(ctx context.Context, parentID *uint) ([]Category, error)
|
||||
}
|
||||
|
||||
// CopyrightRepository defines CRUD methods specific to Copyright.
|
||||
type CopyrightRepository interface {
|
||||
BaseRepository[Copyright]
|
||||
AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error
|
||||
DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error
|
||||
GetByEntity(ctx context.Context, entityID uint, entityType string) ([]Copyright, error)
|
||||
GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]Copyrightable, error)
|
||||
AddTranslation(ctx context.Context, translation *CopyrightTranslation) error
|
||||
GetTranslations(ctx context.Context, copyrightID uint) ([]CopyrightTranslation, error)
|
||||
GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*CopyrightTranslation, error)
|
||||
}
|
||||
1
internal/domain/work/.keep
Normal file
1
internal/domain/work/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
1
internal/jobs/linguistics/.keep
Normal file
1
internal/jobs/linguistics/.keep
Normal file
@ -0,0 +1 @@
|
||||
# This file is created to ensure the directory structure is in place.
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import (
|
||||
"sort"
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import "testing"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import "testing"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import "testing"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import "testing"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
import "testing"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package enrich
|
||||
package linguistics
|
||||
|
||||
// Registry holds all the text analysis services
|
||||
type Registry struct {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user