tercul-backend/internal/adapters/graphql/schema.resolvers.go
google-labs-jules[bot] caf07df08d feat(analytics): Enhance analytics capabilities
This commit introduces a comprehensive enhancement of the application's analytics features, addressing performance, data modeling, and feature set.

The key changes include:

- **Performance Improvement:** The analytics repository now uses a database "UPSERT" operation to increment counters, reducing two separate database calls (read and write) into a single, more efficient operation.

- **New Metrics:** The `WorkStats` and `TranslationStats` models have been enriched with new, calculated metrics:
  - `ReadingTime`: An estimation of the time required to read the work or translation.
  - `Complexity`: A score representing the linguistic complexity of the text.
  - `Sentiment`: A score indicating the emotional tone of the text.

- **Service Refactoring:** The analytics service has been refactored to support the new metrics. It now includes methods to calculate and update these scores, leveraging the existing linguistics package for text analysis.

- **GraphQL API Expansion:** The new analytics fields (`readingTime`, `complexity`, `sentiment`) have been exposed through the GraphQL API by updating the `WorkStats` and `TranslationStats` types in the schema.

- **Validation and Testing:**
  - GraphQL input validation has been centralized and improved by moving from ad-hoc checks to a consistent validation pattern in the GraphQL layer.
  - The test suite has been significantly improved with the addition of new tests for the analytics service and the data access layer, ensuring the correctness and robustness of the new features. This includes fixing several bugs that were discovered during the development process.
2025-09-07 19:26:51 +00:00

1359 lines
39 KiB
Go

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.
// Code generated by github.com/99designs/gqlgen version v0.17.78
import (
"context"
"fmt"
"log"
"strconv"
"tercul/internal/adapters/graphql/model"
"tercul/internal/app/auth"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
)
// 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 := auth.RegisterInput{
Username: input.Username,
Email: input.Email,
Password: input.Password,
FirstName: input.FirstName,
LastName: input.LastName,
}
// Call auth service
authResponse, err := r.App.AuthCommands.Register(ctx, registerInput)
if err != nil {
return nil, err
}
// Convert service response to GraphQL response
return &model.AuthPayload{
Token: authResponse.Token,
User: &model.User{
ID: fmt.Sprintf("%d", authResponse.User.ID),
Username: authResponse.User.Username,
Email: authResponse.User.Email,
FirstName: &authResponse.User.FirstName,
LastName: &authResponse.User.LastName,
DisplayName: &authResponse.User.DisplayName,
Role: model.UserRole(authResponse.User.Role),
Verified: authResponse.User.Verified,
Active: authResponse.User.Active,
},
}, nil
}
// Login is the resolver for the login field.
func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*model.AuthPayload, error) {
// Convert GraphQL input to service input
loginInput := auth.LoginInput{
Email: input.Email,
Password: input.Password,
}
// Call auth service
authResponse, err := r.App.AuthCommands.Login(ctx, loginInput)
if err != nil {
return nil, err
}
// Convert service response to GraphQL response
return &model.AuthPayload{
Token: authResponse.Token,
User: &model.User{
ID: fmt.Sprintf("%d", authResponse.User.ID),
Username: authResponse.User.Username,
Email: authResponse.User.Email,
FirstName: &authResponse.User.FirstName,
LastName: &authResponse.User.LastName,
DisplayName: &authResponse.User.DisplayName,
Role: model.UserRole(authResponse.User.Role),
Verified: authResponse.User.Verified,
Active: authResponse.User.Active,
},
}, nil
}
// CreateWork is the resolver for the createWork field.
func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput) (*model.Work, error) {
if err := validateWorkInput(input); err != nil {
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
}
// Create domain model
work := &domain.Work{
Title: input.Name,
TranslatableModel: domain.TranslatableModel{Language: input.Language},
// Description: *input.Description,
// Other fields can be set here
}
// Call work service
err := r.App.WorkCommands.CreateWork(ctx, work)
if err != nil {
return nil, 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)
// }
*/
}
// Convert to GraphQL model
return &model.Work{
ID: fmt.Sprintf("%d", work.ID),
Name: work.Title,
Language: work.Language,
Content: input.Content,
}, nil
}
// UpdateWork is the resolver for the updateWork field.
func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input model.WorkInput) (*model.Work, error) {
if err := validateWorkInput(input); err != nil {
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
}
workID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
// Create domain model
work := &domain.Work{
TranslatableModel: domain.TranslatableModel{
BaseModel: domain.BaseModel{ID: uint(workID)},
Language: input.Language,
},
Title: input.Name,
}
// Call work service
err = r.App.WorkCommands.UpdateWork(ctx, work)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Work{
ID: id,
Name: work.Title,
Language: work.Language,
Content: input.Content,
}, nil
}
// DeleteWork is the resolver for the deleteWork field.
func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) {
workID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid work ID: %v", err)
}
err = r.App.WorkCommands.DeleteWork(ctx, uint(workID))
if err != nil {
return false, err
}
return true, nil
}
// CreateTranslation is the resolver for the createTranslation field.
func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.TranslationInput) (*model.Translation, error) {
if err := validateTranslationInput(input); err != nil {
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
}
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
// Create domain model
translation := &domain.Translation{
Title: input.Name,
Language: input.Language,
TranslatableID: uint(workID),
TranslatableType: "Work",
}
if input.Content != nil {
translation.Content = *input.Content
}
// Call translation service
err = r.App.TranslationRepo.Create(ctx, translation)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Translation{
ID: fmt.Sprintf("%d", translation.ID),
Name: translation.Title,
Language: translation.Language,
Content: &translation.Content,
WorkID: input.WorkID,
}, nil
}
// UpdateTranslation is the resolver for the updateTranslation field.
func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, input model.TranslationInput) (*model.Translation, error) {
if err := validateTranslationInput(input); err != nil {
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
}
translationID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
// Create domain model
translation := &domain.Translation{
BaseModel: domain.BaseModel{ID: uint(translationID)},
Title: input.Name,
Language: input.Language,
TranslatableID: uint(workID),
TranslatableType: "Work",
}
if input.Content != nil {
translation.Content = *input.Content
}
// Call translation service
err = r.App.TranslationRepo.Update(ctx, translation)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Translation{
ID: id,
Name: translation.Title,
Language: translation.Language,
Content: &translation.Content,
WorkID: input.WorkID,
}, nil
}
// DeleteTranslation is the resolver for the deleteTranslation field.
func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) {
translationID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid translation ID: %v", err)
}
err = r.App.TranslationRepo.Delete(ctx, uint(translationID))
if err != nil {
return false, err
}
return true, nil
}
// CreateAuthor is the resolver for the createAuthor field.
func (r *mutationResolver) CreateAuthor(ctx context.Context, input model.AuthorInput) (*model.Author, error) {
if err := validateAuthorInput(input); err != nil {
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
}
// Create domain model
author := &domain.Author{
Name: input.Name,
TranslatableModel: domain.TranslatableModel{
Language: input.Language,
},
}
// Call author service
err := r.App.AuthorRepo.Create(ctx, author)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Author{
ID: fmt.Sprintf("%d", author.ID),
Name: author.Name,
Language: author.Language,
}, nil
}
// UpdateAuthor is the resolver for the updateAuthor field.
func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input model.AuthorInput) (*model.Author, error) {
if err := validateAuthorInput(input); err != nil {
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
}
authorID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid author ID: %v", err)
}
// Create domain model
author := &domain.Author{
TranslatableModel: domain.TranslatableModel{
BaseModel: domain.BaseModel{ID: uint(authorID)},
Language: input.Language,
},
Name: input.Name,
}
// Call author service
err = r.App.AuthorRepo.Update(ctx, author)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Author{
ID: id,
Name: author.Name,
Language: author.Language,
}, nil
}
// DeleteAuthor is the resolver for the deleteAuthor field.
func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) {
authorID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid author ID: %v", err)
}
err = r.App.AuthorRepo.Delete(ctx, uint(authorID))
if err != nil {
return false, err
}
return true, nil
}
// UpdateUser is the resolver for the updateUser field.
func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input model.UserInput) (*model.User, error) {
panic(fmt.Errorf("not implemented: UpdateUser - updateUser"))
}
// DeleteUser is the resolver for the deleteUser field.
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
panic(fmt.Errorf("not implemented: DeleteUser - deleteUser"))
}
// CreateCollection is the resolver for the createCollection field.
func (r *mutationResolver) CreateCollection(ctx context.Context, input model.CollectionInput) (*model.Collection, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Create domain model
collection := &domain.Collection{
Name: input.Name,
UserID: userID,
}
if input.Description != nil {
collection.Description = *input.Description
}
// Call collection repository
err := r.App.CollectionRepo.Create(ctx, collection)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Collection{
ID: fmt.Sprintf("%d", collection.ID),
Name: collection.Name,
Description: &collection.Description,
User: &model.User{
ID: fmt.Sprintf("%d", userID),
},
}, nil
}
// UpdateCollection is the resolver for the updateCollection field.
func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, input model.CollectionInput) (*model.Collection, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Parse collection ID
collectionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err)
}
// Fetch the existing collection
collection, err := r.App.CollectionRepo.GetByID(ctx, uint(collectionID))
if err != nil {
return nil, err
}
if collection == nil {
return nil, fmt.Errorf("collection not found")
}
// Check ownership
if collection.UserID != userID {
return nil, fmt.Errorf("unauthorized")
}
// Update fields
collection.Name = input.Name
if input.Description != nil {
collection.Description = *input.Description
}
// Call collection repository
err = r.App.CollectionRepo.Update(ctx, collection)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Collection{
ID: id,
Name: collection.Name,
Description: &collection.Description,
User: &model.User{
ID: fmt.Sprintf("%d", userID),
},
}, nil
}
// DeleteCollection is the resolver for the deleteCollection field.
func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (bool, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, fmt.Errorf("unauthorized")
}
// Parse collection ID
collectionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid collection ID: %v", err)
}
// Fetch the existing collection
collection, err := r.App.CollectionRepo.GetByID(ctx, uint(collectionID))
if err != nil {
return false, err
}
if collection == nil {
return false, fmt.Errorf("collection not found")
}
// Check ownership
if collection.UserID != userID {
return false, fmt.Errorf("unauthorized")
}
// Call collection repository
err = r.App.CollectionRepo.Delete(ctx, uint(collectionID))
if err != nil {
return false, err
}
return true, nil
}
// AddWorkToCollection is the resolver for the addWorkToCollection field.
func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID string, workID string) (*model.Collection, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Parse IDs
collID, err := strconv.ParseUint(collectionID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err)
}
wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
// Fetch the existing collection
collection, err := r.App.CollectionRepo.GetByID(ctx, uint(collID))
if err != nil {
return nil, err
}
if collection == nil {
return nil, fmt.Errorf("collection not found")
}
// Check ownership
if collection.UserID != userID {
return nil, fmt.Errorf("unauthorized")
}
// Add work to collection
err = r.App.CollectionRepo.AddWorkToCollection(ctx, uint(collID), uint(wID))
if err != nil {
return nil, err
}
// Fetch the updated collection to return it
updatedCollection, err := r.App.CollectionRepo.GetByID(ctx, uint(collID))
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Collection{
ID: collectionID,
Name: updatedCollection.Name,
Description: &updatedCollection.Description,
}, nil
}
// RemoveWorkFromCollection is the resolver for the removeWorkFromCollection field.
func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collectionID string, workID string) (*model.Collection, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Parse IDs
collID, err := strconv.ParseUint(collectionID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err)
}
wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
// Fetch the existing collection
collection, err := r.App.CollectionRepo.GetByID(ctx, uint(collID))
if err != nil {
return nil, err
}
if collection == nil {
return nil, fmt.Errorf("collection not found")
}
// Check ownership
if collection.UserID != userID {
return nil, fmt.Errorf("unauthorized")
}
// Remove work from collection
err = r.App.CollectionRepo.RemoveWorkFromCollection(ctx, uint(collID), uint(wID))
if err != nil {
return nil, err
}
// Fetch the updated collection to return it
updatedCollection, err := r.App.CollectionRepo.GetByID(ctx, uint(collID))
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Collection{
ID: collectionID,
Name: updatedCollection.Name,
Description: &updatedCollection.Description,
}, nil
}
// CreateComment is the resolver for the createComment field.
func (r *mutationResolver) CreateComment(ctx context.Context, input model.CommentInput) (*model.Comment, error) {
// Custom validation
if (input.WorkID == nil && input.TranslationID == nil) || (input.WorkID != nil && input.TranslationID != nil) {
return nil, fmt.Errorf("must provide either workId or translationId, but not both")
}
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Create domain model
comment := &domain.Comment{
Text: input.Text,
UserID: userID,
}
if input.WorkID != nil {
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
wID := uint(workID)
comment.WorkID = &wID
}
if input.TranslationID != nil {
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
tID := uint(translationID)
comment.TranslationID = &tID
}
if input.ParentCommentID != nil {
parentCommentID, err := strconv.ParseUint(*input.ParentCommentID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid parent comment ID: %v", err)
}
pID := uint(parentCommentID)
comment.ParentID = &pID
}
// Call comment repository
err := r.App.CommentRepo.Create(ctx, comment)
if err != nil {
return nil, err
}
// Increment analytics
if comment.WorkID != nil {
r.App.AnalyticsService.IncrementWorkComments(ctx, *comment.WorkID)
}
if comment.TranslationID != nil {
r.App.AnalyticsService.IncrementTranslationComments(ctx, *comment.TranslationID)
}
// Convert to GraphQL model
return &model.Comment{
ID: fmt.Sprintf("%d", comment.ID),
Text: comment.Text,
User: &model.User{
ID: fmt.Sprintf("%d", userID),
},
}, nil
}
// UpdateComment is the resolver for the updateComment field.
func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input model.CommentInput) (*model.Comment, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Parse comment ID
commentID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid comment ID: %v", err)
}
// Fetch the existing comment
comment, err := r.App.CommentRepo.GetByID(ctx, uint(commentID))
if err != nil {
return nil, err
}
if comment == nil {
return nil, fmt.Errorf("comment not found")
}
// Check ownership
if comment.UserID != userID {
return nil, fmt.Errorf("unauthorized")
}
// Update fields
comment.Text = input.Text
// Call comment repository
err = r.App.CommentRepo.Update(ctx, comment)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Comment{
ID: id,
Text: comment.Text,
User: &model.User{
ID: fmt.Sprintf("%d", userID),
},
}, nil
}
// DeleteComment is the resolver for the deleteComment field.
func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, fmt.Errorf("unauthorized")
}
// Parse comment ID
commentID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid comment ID: %v", err)
}
// Fetch the existing comment
comment, err := r.App.CommentRepo.GetByID(ctx, uint(commentID))
if err != nil {
return false, err
}
if comment == nil {
return false, fmt.Errorf("comment not found")
}
// Check ownership
if comment.UserID != userID {
return false, fmt.Errorf("unauthorized")
}
// Call comment repository
err = r.App.CommentRepo.Delete(ctx, uint(commentID))
if err != nil {
return false, err
}
return true, nil
}
// CreateLike is the resolver for the createLike field.
func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput) (*model.Like, error) {
// Custom validation
if (input.WorkID == nil && input.TranslationID == nil && input.CommentID == nil) ||
(input.WorkID != nil && input.TranslationID != nil) ||
(input.WorkID != nil && input.CommentID != nil) ||
(input.TranslationID != nil && input.CommentID != nil) {
return nil, fmt.Errorf("must provide exactly one of workId, translationId, or commentId")
}
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Create domain model
like := &domain.Like{
UserID: userID,
}
if input.WorkID != nil {
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
wID := uint(workID)
like.WorkID = &wID
}
if input.TranslationID != nil {
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
tID := uint(translationID)
like.TranslationID = &tID
}
if input.CommentID != nil {
commentID, err := strconv.ParseUint(*input.CommentID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid comment ID: %v", err)
}
cID := uint(commentID)
like.CommentID = &cID
}
// Call like repository
err := r.App.LikeRepo.Create(ctx, like)
if err != nil {
return nil, err
}
// Increment analytics
if like.WorkID != nil {
r.App.AnalyticsService.IncrementWorkLikes(ctx, *like.WorkID)
}
if like.TranslationID != nil {
r.App.AnalyticsService.IncrementTranslationLikes(ctx, *like.TranslationID)
}
// Convert to GraphQL model
return &model.Like{
ID: fmt.Sprintf("%d", like.ID),
User: &model.User{ID: fmt.Sprintf("%d", userID)},
}, nil
}
// DeleteLike is the resolver for the deleteLike field.
func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, fmt.Errorf("unauthorized")
}
// Parse like ID
likeID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid like ID: %v", err)
}
// Fetch the existing like
like, err := r.App.LikeRepo.GetByID(ctx, uint(likeID))
if err != nil {
return false, err
}
if like == nil {
return false, fmt.Errorf("like not found")
}
// Check ownership
if like.UserID != userID {
return false, fmt.Errorf("unauthorized")
}
// Call like repository
err = r.App.LikeRepo.Delete(ctx, uint(likeID))
if err != nil {
return false, err
}
return true, nil
}
// CreateBookmark is the resolver for the createBookmark field.
func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.BookmarkInput) (*model.Bookmark, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Parse work ID
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
// Create domain model
bookmark := &domain.Bookmark{
UserID: userID,
WorkID: uint(workID),
}
if input.Name != nil {
bookmark.Name = *input.Name
}
// Call bookmark repository
err = r.App.BookmarkRepo.Create(ctx, bookmark)
if err != nil {
return nil, err
}
// Increment analytics
r.App.AnalyticsService.IncrementWorkBookmarks(ctx, uint(workID))
// Convert to GraphQL model
return &model.Bookmark{
ID: fmt.Sprintf("%d", bookmark.ID),
Name: &bookmark.Name,
User: &model.User{ID: fmt.Sprintf("%d", userID)},
Work: &model.Work{ID: fmt.Sprintf("%d", workID)},
}, nil
}
// DeleteBookmark is the resolver for the deleteBookmark field.
func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool, error) {
// Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, fmt.Errorf("unauthorized")
}
// Parse bookmark ID
bookmarkID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid bookmark ID: %v", err)
}
// Fetch the existing bookmark
bookmark, err := r.App.BookmarkRepo.GetByID(ctx, uint(bookmarkID))
if err != nil {
return false, err
}
if bookmark == nil {
return false, fmt.Errorf("bookmark not found")
}
// Check ownership
if bookmark.UserID != userID {
return false, fmt.Errorf("unauthorized")
}
// Call bookmark repository
err = r.App.BookmarkRepo.Delete(ctx, uint(bookmarkID))
if err != nil {
return false, err
}
return true, nil
}
// CreateContribution is the resolver for the createContribution field.
func (r *mutationResolver) CreateContribution(ctx context.Context, input model.ContributionInput) (*model.Contribution, error) {
panic(fmt.Errorf("not implemented: CreateContribution - createContribution"))
}
// UpdateContribution is the resolver for the updateContribution field.
func (r *mutationResolver) UpdateContribution(ctx context.Context, id string, input model.ContributionInput) (*model.Contribution, error) {
panic(fmt.Errorf("not implemented: UpdateContribution - updateContribution"))
}
// DeleteContribution is the resolver for the deleteContribution field.
func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (bool, error) {
panic(fmt.Errorf("not implemented: DeleteContribution - deleteContribution"))
}
// ReviewContribution is the resolver for the reviewContribution field.
func (r *mutationResolver) ReviewContribution(ctx context.Context, id string, status model.ContributionStatus, feedback *string) (*model.Contribution, error) {
panic(fmt.Errorf("not implemented: ReviewContribution - reviewContribution"))
}
// Logout is the resolver for the logout field.
func (r *mutationResolver) Logout(ctx context.Context) (bool, error) {
panic(fmt.Errorf("not implemented: Logout - logout"))
}
// RefreshToken is the resolver for the refreshToken field.
func (r *mutationResolver) RefreshToken(ctx context.Context) (*model.AuthPayload, error) {
panic(fmt.Errorf("not implemented: RefreshToken - refreshToken"))
}
// ForgotPassword is the resolver for the forgotPassword field.
func (r *mutationResolver) ForgotPassword(ctx context.Context, email string) (bool, error) {
panic(fmt.Errorf("not implemented: ForgotPassword - forgotPassword"))
}
// ResetPassword is the resolver for the resetPassword field.
func (r *mutationResolver) ResetPassword(ctx context.Context, token string, newPassword string) (bool, error) {
panic(fmt.Errorf("not implemented: ResetPassword - resetPassword"))
}
// VerifyEmail is the resolver for the verifyEmail field.
func (r *mutationResolver) VerifyEmail(ctx context.Context, token string) (bool, error) {
panic(fmt.Errorf("not implemented: VerifyEmail - verifyEmail"))
}
// ResendVerificationEmail is the resolver for the resendVerificationEmail field.
func (r *mutationResolver) ResendVerificationEmail(ctx context.Context, email string) (bool, error) {
panic(fmt.Errorf("not implemented: ResendVerificationEmail - resendVerificationEmail"))
}
// UpdateProfile is the resolver for the updateProfile field.
func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserInput) (*model.User, error) {
panic(fmt.Errorf("not implemented: UpdateProfile - updateProfile"))
}
// ChangePassword is the resolver for the changePassword field.
func (r *mutationResolver) ChangePassword(ctx context.Context, currentPassword string, newPassword string) (bool, error) {
panic(fmt.Errorf("not implemented: ChangePassword - changePassword"))
}
// Work is the resolver for the work field.
func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) {
workID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
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
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: &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) {
// 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 {
pageSize = int(*limit)
}
if offset != nil {
page = int(*offset)/pageSize + 1
}
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 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: &content,
})
}
return result, nil
}
// Translation is the resolver for the translation field.
func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Translation, error) {
panic(fmt.Errorf("not implemented: Translation - translation"))
}
// Translations is the resolver for the translations field.
func (r *queryResolver) Translations(ctx context.Context, workID string, language *string, limit *int32, offset *int32) ([]*model.Translation, error) {
panic(fmt.Errorf("not implemented: Translations - translations"))
}
// Author is the resolver for the author field.
func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, error) {
panic(fmt.Errorf("not implemented: Author - author"))
}
// Authors is the resolver for the authors field.
func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32, search *string, countryID *string) ([]*model.Author, error) {
var authors []domain.Author
var err error
if countryID != nil {
countryIDUint, err := strconv.ParseUint(*countryID, 10, 32)
if err != nil {
return nil, err
}
authors, err = r.App.AuthorRepo.ListByCountryID(ctx, uint(countryIDUint))
} else {
result, err := r.App.AuthorRepo.List(ctx, 1, 1000) // Use pagination
if err != nil {
return nil, err
}
authors = result.Items
}
if err != nil {
return nil, err
}
// Convert to GraphQL model; resolve biography via Localization service
var result []*model.Author
for _, a := range authors {
var bio *string
if r.App.Localization != nil {
if b, err := r.App.Localization.GetAuthorBiography(ctx, a.ID, a.Language); err == nil && b != "" {
bio = &b
}
}
result = append(result, &model.Author{
ID: fmt.Sprintf("%d", a.ID),
Name: a.Name,
Language: a.Language,
Biography: bio,
})
}
return result, nil
}
// User is the resolver for the user field.
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
panic(fmt.Errorf("not implemented: User - user"))
}
// UserByEmail is the resolver for the userByEmail field.
func (r *queryResolver) UserByEmail(ctx context.Context, email string) (*model.User, error) {
panic(fmt.Errorf("not implemented: UserByEmail - userByEmail"))
}
// UserByUsername is the resolver for the userByUsername field.
func (r *queryResolver) UserByUsername(ctx context.Context, username string) (*model.User, error) {
panic(fmt.Errorf("not implemented: UserByUsername - userByUsername"))
}
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context, limit *int32, offset *int32, role *model.UserRole) ([]*model.User, error) {
var users []domain.User
var err error
if role != nil {
// Convert GraphQL role to model role
var modelRole domain.UserRole
switch *role {
case model.UserRoleReader:
modelRole = domain.UserRoleReader
case model.UserRoleContributor:
modelRole = domain.UserRoleContributor
case model.UserRoleReviewer:
modelRole = domain.UserRoleReviewer
case model.UserRoleEditor:
modelRole = domain.UserRoleEditor
case model.UserRoleAdmin:
modelRole = domain.UserRoleAdmin
default:
return nil, fmt.Errorf("invalid user role: %s", *role)
}
users, err = r.App.UserRepo.ListByRole(ctx, modelRole)
} else {
result, err := r.App.UserRepo.List(ctx, 1, 1000) // Use pagination
if err != nil {
return nil, err
}
users = result.Items
}
if err != nil {
return nil, err
}
// Convert to GraphQL model
var result []*model.User
for _, u := range users {
// Convert model role to GraphQL role
var graphqlRole model.UserRole
switch u.Role {
case domain.UserRoleReader:
graphqlRole = model.UserRoleReader
case domain.UserRoleContributor:
graphqlRole = model.UserRoleContributor
case domain.UserRoleReviewer:
graphqlRole = model.UserRoleReviewer
case domain.UserRoleEditor:
graphqlRole = model.UserRoleEditor
case domain.UserRoleAdmin:
graphqlRole = model.UserRoleAdmin
default:
graphqlRole = model.UserRoleReader
}
result = append(result, &model.User{
ID: fmt.Sprintf("%d", u.ID),
Username: u.Username,
Email: u.Email,
Role: graphqlRole,
})
}
return result, nil
}
// Me is the resolver for the me field.
func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
panic(fmt.Errorf("not implemented: Me - me"))
}
// UserProfile is the resolver for the userProfile field.
func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) {
panic(fmt.Errorf("not implemented: UserProfile - userProfile"))
}
// Collection is the resolver for the collection field.
func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Collection, error) {
panic(fmt.Errorf("not implemented: Collection - collection"))
}
// Collections is the resolver for the collections field.
func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *int32, offset *int32) ([]*model.Collection, error) {
panic(fmt.Errorf("not implemented: Collections - collections"))
}
// Tag is the resolver for the tag field.
func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) {
tagID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, err
}
tag, err := r.App.TagRepo.GetByID(ctx, uint(tagID))
if err != nil {
return nil, err
}
return &model.Tag{
ID: fmt.Sprintf("%d", tag.ID),
Name: tag.Name,
}, nil
}
// Tags is the resolver for the tags field.
func (r *queryResolver) Tags(ctx context.Context, limit *int32, offset *int32) ([]*model.Tag, error) {
paginatedResult, err := r.App.TagRepo.List(ctx, 1, 1000) // Use pagination
if err != nil {
return nil, err
}
// Convert to GraphQL model
var result []*model.Tag
for _, t := range paginatedResult.Items {
result = append(result, &model.Tag{
ID: fmt.Sprintf("%d", t.ID),
Name: t.Name,
})
}
return result, nil
}
// Category is the resolver for the category field.
func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) {
categoryID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, err
}
category, err := r.App.CategoryRepo.GetByID(ctx, uint(categoryID))
if err != nil {
return nil, err
}
return &model.Category{
ID: fmt.Sprintf("%d", category.ID),
Name: category.Name,
}, nil
}
// Categories is the resolver for the categories field.
func (r *queryResolver) Categories(ctx context.Context, limit *int32, offset *int32) ([]*model.Category, error) {
paginatedResult, err := r.App.CategoryRepo.List(ctx, 1, 1000)
if err != nil {
return nil, err
}
// Convert to GraphQL model
var result []*model.Category
for _, c := range paginatedResult.Items {
result = append(result, &model.Category{
ID: fmt.Sprintf("%d", c.ID),
Name: c.Name,
})
}
return result, nil
}
// Comment is the resolver for the comment field.
func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment, error) {
panic(fmt.Errorf("not implemented: Comment - comment"))
}
// Comments is the resolver for the comments field.
func (r *queryResolver) Comments(ctx context.Context, workID *string, translationID *string, userID *string, limit *int32, offset *int32) ([]*model.Comment, error) {
panic(fmt.Errorf("not implemented: Comments - comments"))
}
// Search is the resolver for the search field.
func (r *queryResolver) Search(ctx context.Context, query string, limit *int32, offset *int32, filters *model.SearchFilters) (*model.SearchResults, error) {
panic(fmt.Errorf("not implemented: Search - search"))
}
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
// Work returns WorkResolver implementation.
func (r *Resolver) Work() WorkResolver { return &workResolver{r} }
// Translation returns TranslationResolver implementation.
func (r *Resolver) Translation() TranslationResolver { return &translationResolver{r} }
type workResolver struct{ *Resolver }
type translationResolver struct{ *Resolver }
func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkStats, error) {
workID, err := strconv.ParseUint(obj.ID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
stats, err := r.App.AnalyticsService.GetOrCreateWorkStats(ctx, uint(workID))
if err != nil {
return nil, err
}
// Convert domain model to GraphQL model
return &model.WorkStats{
ID: fmt.Sprintf("%d", stats.ID),
Views: &stats.Views,
Likes: &stats.Likes,
Comments: &stats.Comments,
Bookmarks: &stats.Bookmarks,
Shares: &stats.Shares,
TranslationCount: &stats.TranslationCount,
ReadingTime: &stats.ReadingTime,
Complexity: &stats.Complexity,
Sentiment: &stats.Sentiment,
}, nil
}
func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) {
translationID, err := strconv.ParseUint(obj.ID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
stats, err := r.App.AnalyticsService.GetOrCreateTranslationStats(ctx, uint(translationID))
if err != nil {
return nil, err
}
// Convert domain model to GraphQL model
return &model.TranslationStats{
ID: fmt.Sprintf("%d", stats.ID),
Views: &stats.Views,
Likes: &stats.Likes,
Comments: &stats.Comments,
Shares: &stats.Shares,
ReadingTime: &stats.ReadingTime,
Sentiment: &stats.Sentiment,
}, nil
}