tercul-backend/internal/adapters/graphql/schema.resolvers.go
google-labs-jules[bot] c2e9a118e2 feat(testing): Increase test coverage and fix authz bugs
This commit significantly increases the test coverage across the application and fixes several underlying bugs that were discovered while writing the new tests.

The key changes include:

- **New Tests:** Added extensive integration and unit tests for GraphQL resolvers, application services, and data repositories, substantially increasing the test coverage for packages like `graphql`, `user`, `translation`, and `analytics`.

- **Authorization Bug Fixes:**
  - Fixed a critical bug where a user creating a `Work` was not correctly associated as its author, causing subsequent permission failures.
  - Corrected the authorization logic in `authz.Service` to properly check for entity ownership by non-admin users.

- **Test Refactoring:**
  - Refactored numerous test suites to use `testify/mock` instead of manual mocks, improving test clarity and maintainability.
  - Isolated integration tests by creating a fresh admin user and token for each test run, eliminating test pollution.
  - Centralized domain errors into `internal/domain/errors.go` and updated repositories to use them, making error handling more consistent.

- **Code Quality Improvements:**
  - Replaced manual mock implementations with `testify/mock` for better consistency.
  - Cleaned up redundant and outdated test files.

These changes stabilize the test suite, improve the overall quality of the codebase, and move the project closer to the goal of 80% test coverage.
2025-10-09 07:03:45 +00:00

2078 lines
56 KiB
Go

package graphql
import (
"context"
"errors"
"fmt"
"strconv"
"tercul/internal/adapters/graphql/model"
"tercul/internal/app/auth"
"tercul/internal/app/author"
"tercul/internal/app/book"
"tercul/internal/app/bookmark"
"tercul/internal/app/collection"
"tercul/internal/app/comment"
"tercul/internal/app/contribution"
"tercul/internal/app/like"
"tercul/internal/app/translation"
"tercul/internal/app/user"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
"tercul/internal/platform/log"
"time"
)
// 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.Auth.Commands.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.Auth.Commands.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 := Validate(input); err != nil {
return nil, err
}
// Create domain model
workModel := &domain.Work{
Title: input.Name,
TranslatableModel: domain.TranslatableModel{Language: input.Language},
}
// Call work service
createdWork, err := r.App.Work.Commands.CreateWork(ctx, workModel)
if err != nil {
return nil, err
}
if input.Content != nil && *input.Content != "" {
translationInput := translation.CreateOrUpdateTranslationInput{
Title: input.Name,
Content: *input.Content,
Language: input.Language,
TranslatableID: createdWork.ID,
TranslatableType: "works",
IsOriginalLanguage: true,
}
_, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, translationInput)
if err != nil {
return nil, fmt.Errorf("failed to create initial translation: %w", err)
}
}
// Convert to GraphQL model
return &model.Work{
ID: fmt.Sprintf("%d", createdWork.ID),
Name: createdWork.Title,
Language: createdWork.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 := Validate(input); err != nil {
return nil, err
}
workID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
}
// Create domain model
workModel := &domain.Work{
TranslatableModel: domain.TranslatableModel{
BaseModel: domain.BaseModel{ID: uint(workID)},
Language: input.Language,
},
Title: input.Name,
}
// Call work service
err = r.App.Work.Commands.UpdateWork(ctx, workModel)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Work{
ID: id,
Name: workModel.Title,
Language: workModel.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("%w: invalid work ID", domain.ErrValidation)
}
err = r.App.Work.Commands.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 := Validate(input); err != nil {
return nil, err
}
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
}
var content string
if input.Content != nil {
content = *input.Content
}
createInput := translation.CreateOrUpdateTranslationInput{
Title: input.Name,
Content: content,
Language: input.Language,
TranslatableID: uint(workID),
TranslatableType: "works",
}
createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput)
if err != nil {
return nil, err
}
go func() {
if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), uint(workID)); err != nil {
log.Error(err, "failed to increment work translation count")
}
}()
return &model.Translation{
ID: fmt.Sprintf("%d", createdTranslation.ID),
Name: createdTranslation.Title,
Language: createdTranslation.Language,
Content: &createdTranslation.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 := Validate(input); err != nil {
return nil, err
}
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
}
var content string
if input.Content != nil {
content = *input.Content
}
updateInput := translation.CreateOrUpdateTranslationInput{
Title: input.Name,
Content: content,
Language: input.Language,
TranslatableID: uint(workID),
TranslatableType: "works",
}
updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput)
if err != nil {
return nil, err
}
return &model.Translation{
ID: fmt.Sprintf("%d", updatedTranslation.ID),
Name: updatedTranslation.Title,
Language: updatedTranslation.Language,
Content: &updatedTranslation.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.Translation.Commands.DeleteTranslation(ctx, uint(translationID))
if err != nil {
return false, err
}
return true, nil
}
// CreateBook is the resolver for the createBook field.
func (r *mutationResolver) CreateBook(ctx context.Context, input model.BookInput) (*model.Book, error) {
if err := Validate(input); err != nil {
return nil, err
}
createInput := book.CreateBookInput{
Title: input.Name,
Description: *input.Description,
Language: input.Language,
ISBN: input.Isbn,
}
createdBook, err := r.App.Book.Commands.CreateBook(ctx, createInput)
if err != nil {
return nil, err
}
return &model.Book{
ID: fmt.Sprintf("%d", createdBook.ID),
Name: createdBook.Title,
Language: createdBook.Language,
Description: &createdBook.Description,
Isbn: &createdBook.ISBN,
}, nil
}
// UpdateBook is the resolver for the updateBook field.
func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input model.BookInput) (*model.Book, error) {
if err := Validate(input); err != nil {
return nil, err
}
bookID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
}
updateInput := book.UpdateBookInput{
ID: uint(bookID),
Title: &input.Name,
Description: input.Description,
Language: &input.Language,
ISBN: input.Isbn,
}
updatedBook, err := r.App.Book.Commands.UpdateBook(ctx, updateInput)
if err != nil {
return nil, err
}
return &model.Book{
ID: id,
Name: updatedBook.Title,
Language: updatedBook.Language,
Description: &updatedBook.Description,
Isbn: &updatedBook.ISBN,
}, nil
}
// DeleteBook is the resolver for the deleteBook field.
func (r *mutationResolver) DeleteBook(ctx context.Context, id string) (bool, error) {
bookID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
}
err = r.App.Book.Commands.DeleteBook(ctx, uint(bookID))
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 := Validate(input); err != nil {
return nil, err
}
createInput := author.CreateAuthorInput{
Name: input.Name,
}
createdAuthor, err := r.App.Author.Commands.CreateAuthor(ctx, createInput)
if err != nil {
return nil, err
}
return &model.Author{
ID: fmt.Sprintf("%d", createdAuthor.ID),
Name: createdAuthor.Name,
Language: createdAuthor.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 := Validate(input); err != nil {
return nil, err
}
authorID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid author ID: %v", err)
}
updateInput := author.UpdateAuthorInput{
ID: uint(authorID),
Name: input.Name,
}
updatedAuthor, err := r.App.Author.Commands.UpdateAuthor(ctx, updateInput)
if err != nil {
return nil, err
}
return &model.Author{
ID: id,
Name: updatedAuthor.Name,
Language: updatedAuthor.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.Author.Commands.DeleteAuthor(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) {
if err := Validate(input); err != nil {
return nil, err
}
userID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid user ID: %v", err)
}
updateInput := user.UpdateUserInput{
ID: uint(userID),
Username: input.Username,
Email: input.Email,
Password: input.Password,
FirstName: input.FirstName,
LastName: input.LastName,
DisplayName: input.DisplayName,
Bio: input.Bio,
AvatarURL: input.AvatarURL,
Verified: input.Verified,
Active: input.Active,
}
if input.Role != nil {
role := domain.UserRole(input.Role.String())
updateInput.Role = &role
}
if input.CountryID != nil {
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid country ID: %v", err)
}
uid := uint(countryID)
updateInput.CountryID = &uid
}
if input.CityID != nil {
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid city ID: %v", err)
}
uid := uint(cityID)
updateInput.CityID = &uid
}
if input.AddressID != nil {
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid address ID: %v", err)
}
uid := uint(addressID)
updateInput.AddressID = &uid
}
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
if err != nil {
return nil, err
}
return &model.User{
ID: fmt.Sprintf("%d", updatedUser.ID),
Username: updatedUser.Username,
Email: updatedUser.Email,
FirstName: &updatedUser.FirstName,
LastName: &updatedUser.LastName,
DisplayName: &updatedUser.DisplayName,
Bio: &updatedUser.Bio,
AvatarURL: &updatedUser.AvatarURL,
Role: model.UserRole(updatedUser.Role),
Verified: updatedUser.Verified,
Active: updatedUser.Active,
}, nil
}
// DeleteUser is the resolver for the deleteUser field.
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
userID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
}
err = r.App.User.Commands.DeleteUser(ctx, uint(userID))
if err != nil {
return false, err
}
return true, nil
}
// CreateCollection is the resolver for the createCollection field.
func (r *mutationResolver) CreateCollection(ctx context.Context, input model.CollectionInput) (*model.Collection, error) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
createInput := collection.CreateCollectionInput{
Name: input.Name,
UserID: userID,
}
if input.Description != nil {
createInput.Description = *input.Description
}
createdCollection, err := r.App.Collection.Commands.CreateCollection(ctx, createInput)
if err != nil {
return nil, err
}
return &model.Collection{
ID: fmt.Sprintf("%d", createdCollection.ID),
Name: createdCollection.Name,
Description: &createdCollection.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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
collectionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err)
}
updateInput := collection.UpdateCollectionInput{
ID: uint(collectionID),
Name: input.Name,
UserID: userID,
}
if input.Description != nil {
updateInput.Description = *input.Description
}
updatedCollection, err := r.App.Collection.Commands.UpdateCollection(ctx, updateInput)
if err != nil {
return nil, err
}
return &model.Collection{
ID: id,
Name: updatedCollection.Name,
Description: &updatedCollection.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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, fmt.Errorf("unauthorized")
}
collectionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid collection ID: %v", err)
}
err = r.App.Collection.Commands.DeleteCollection(ctx, uint(collectionID), userID)
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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
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)
}
addInput := collection.AddWorkToCollectionInput{
CollectionID: uint(collID),
WorkID: uint(wID),
UserID: userID,
}
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
if err != nil {
return nil, err
}
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
if err != nil {
return nil, err
}
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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
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)
}
removeInput := collection.RemoveWorkFromCollectionInput{
CollectionID: uint(collID),
WorkID: uint(wID),
UserID: userID,
}
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
if err != nil {
return nil, err
}
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
if err != nil {
return nil, err
}
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) {
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")
}
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
createInput := comment.CreateCommentInput{
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)
createInput.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)
createInput.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)
createInput.ParentID = &pID
}
createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput)
if err != nil {
return nil, err
}
if createdComment.WorkID != nil {
if err := r.App.Analytics.IncrementWorkComments(ctx, *createdComment.WorkID); err != nil {
log.FromContext(ctx).Error(err, "failed to increment work comments")
}
}
if createdComment.TranslationID != nil {
if err := r.App.Analytics.IncrementTranslationComments(ctx, *createdComment.TranslationID); err != nil {
log.FromContext(ctx).Error(err, "failed to increment translation comments")
}
}
return &model.Comment{
ID: fmt.Sprintf("%d", createdComment.ID),
Text: createdComment.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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
commentID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid comment ID: %v", err)
}
commentModel, err := r.App.Comment.Queries.Comment(ctx, uint(commentID))
if err != nil {
return nil, err
}
if commentModel == nil {
return nil, fmt.Errorf("comment not found")
}
if commentModel.UserID != userID {
return nil, fmt.Errorf("unauthorized")
}
updateInput := comment.UpdateCommentInput{
ID: uint(commentID),
Text: input.Text,
}
updatedComment, err := r.App.Comment.Commands.UpdateComment(ctx, updateInput)
if err != nil {
return nil, err
}
return &model.Comment{
ID: id,
Text: updatedComment.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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, fmt.Errorf("unauthorized")
}
commentID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid comment ID: %v", err)
}
comment, err := r.App.Comment.Queries.Comment(ctx, uint(commentID))
if err != nil {
return false, err
}
if comment == nil {
return false, fmt.Errorf("comment not found")
}
if comment.UserID != userID {
return false, fmt.Errorf("unauthorized")
}
err = r.App.Comment.Commands.DeleteComment(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) {
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")
}
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
createInput := like.CreateLikeInput{
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)
createInput.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)
createInput.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)
createInput.CommentID = &cID
}
createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput)
if err != nil {
return nil, err
}
if createdLike.WorkID != nil {
if err := r.App.Analytics.IncrementWorkLikes(ctx, *createdLike.WorkID); err != nil {
log.FromContext(ctx).Error(err, "failed to increment work likes")
}
}
if createdLike.TranslationID != nil {
if err := r.App.Analytics.IncrementTranslationLikes(ctx, *createdLike.TranslationID); err != nil {
log.FromContext(ctx).Error(err, "failed to increment translation likes")
}
}
return &model.Like{
ID: fmt.Sprintf("%d", createdLike.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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, fmt.Errorf("unauthorized")
}
likeID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid like ID: %v", err)
}
like, err := r.App.Like.Queries.Like(ctx, uint(likeID))
if err != nil {
return false, err
}
if like == nil {
return false, fmt.Errorf("like not found")
}
if like.UserID != userID {
return false, fmt.Errorf("unauthorized")
}
err = r.App.Like.Commands.DeleteLike(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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
createInput := bookmark.CreateBookmarkInput{
UserID: userID,
WorkID: uint(workID),
}
if input.Name != nil {
createInput.Name = *input.Name
}
createdBookmark, err := r.App.Bookmark.Commands.CreateBookmark(ctx, createInput)
if err != nil {
return nil, err
}
if err := r.App.Analytics.IncrementWorkBookmarks(ctx, uint(workID)); err != nil {
log.FromContext(ctx).Error(err, "failed to increment work bookmarks")
}
return &model.Bookmark{
ID: fmt.Sprintf("%d", createdBookmark.ID),
Name: &createdBookmark.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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, fmt.Errorf("unauthorized")
}
bookmarkID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("invalid bookmark ID: %v", err)
}
bookmark, err := r.App.Bookmark.Queries.Bookmark(ctx, uint(bookmarkID))
if err != nil {
return false, err
}
if bookmark == nil {
return false, fmt.Errorf("bookmark not found")
}
if bookmark.UserID != userID {
return false, fmt.Errorf("unauthorized")
}
err = r.App.Bookmark.Commands.DeleteBookmark(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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
createInput := contribution.CreateContributionInput{
Name: input.Name,
}
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)
createInput.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)
createInput.TranslationID = &tID
}
if input.Status != nil {
createInput.Status = input.Status.String()
} else {
createInput.Status = "DRAFT" // Default status
}
createdContribution, err := r.App.Contribution.Commands.CreateContribution(ctx, createInput)
if err != nil {
return nil, err
}
return &model.Contribution{
ID: fmt.Sprintf("%d", createdContribution.ID),
Name: createdContribution.Name,
Status: model.ContributionStatus(createdContribution.Status),
User: &model.User{
ID: fmt.Sprintf("%d", userID),
},
}, nil
}
// UpdateContribution is the resolver for the updateContribution field.
func (r *mutationResolver) UpdateContribution(ctx context.Context, id string, input model.ContributionInput) (*model.Contribution, error) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, domain.ErrUnauthorized
}
contributionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
}
updateInput := contribution.UpdateContributionInput{
ID: uint(contributionID),
UserID: userID,
Name: &input.Name,
}
if input.Status != nil {
status := input.Status.String()
updateInput.Status = &status
}
updatedContribution, err := r.App.Contribution.Commands.UpdateContribution(ctx, updateInput)
if err != nil {
return nil, err
}
return &model.Contribution{
ID: fmt.Sprintf("%d", updatedContribution.ID),
Name: updatedContribution.Name,
Status: model.ContributionStatus(updatedContribution.Status),
User: &model.User{
ID: fmt.Sprintf("%d", updatedContribution.UserID),
},
}, nil
}
// DeleteContribution is the resolver for the deleteContribution field.
func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (bool, error) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
contributionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
}
err = r.App.Contribution.Commands.DeleteContribution(ctx, uint(contributionID), userID)
if err != nil {
return false, err
}
return true, nil
}
// 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) {
contributionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
}
reviewInput := contribution.ReviewContributionInput{
ID: uint(contributionID),
Status: status.String(),
Feedback: feedback,
}
reviewedContribution, err := r.App.Contribution.Commands.ReviewContribution(ctx, reviewInput)
if err != nil {
return nil, err
}
return &model.Contribution{
ID: fmt.Sprintf("%d", reviewedContribution.ID),
Name: reviewedContribution.Name,
Status: model.ContributionStatus(reviewedContribution.Status),
User: &model.User{
ID: fmt.Sprintf("%d", reviewedContribution.UserID),
},
}, nil
}
// Logout is the resolver for the logout field.
func (r *mutationResolver) Logout(ctx context.Context) (bool, error) {
err := r.App.Auth.Commands.Logout(ctx)
if err != nil {
return false, err
}
return true, nil
}
// RefreshToken is the resolver for the refreshToken field.
func (r *mutationResolver) RefreshToken(ctx context.Context) (*model.AuthPayload, error) {
authResponse, err := r.App.Auth.Commands.RefreshToken(ctx)
if err != nil {
return nil, err
}
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
}
// ForgotPassword is the resolver for the forgotPassword field.
func (r *mutationResolver) ForgotPassword(ctx context.Context, email string) (bool, error) {
err := r.App.Auth.Commands.ForgotPassword(ctx, email)
if err != nil {
return true, nil
}
return true, nil
}
// ResetPassword is the resolver for the resetPassword field.
func (r *mutationResolver) ResetPassword(ctx context.Context, token string, newPassword string) (bool, error) {
resetInput := auth.ResetPasswordInput{
Token: token,
NewPassword: newPassword,
}
err := r.App.Auth.Commands.ResetPassword(ctx, resetInput)
if err != nil {
return false, err
}
return true, nil
}
// VerifyEmail is the resolver for the verifyEmail field.
func (r *mutationResolver) VerifyEmail(ctx context.Context, token string) (bool, error) {
err := r.App.Auth.Commands.VerifyEmail(ctx, token)
if err != nil {
return false, err
}
return true, nil
}
// ResendVerificationEmail is the resolver for the resendVerificationEmail field.
func (r *mutationResolver) ResendVerificationEmail(ctx context.Context, email string) (bool, error) {
err := r.App.Auth.Commands.ResendVerificationEmail(ctx, email)
if err != nil {
return true, nil
}
return true, nil
}
// UpdateProfile is the resolver for the updateProfile field.
func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserInput) (*model.User, error) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, domain.ErrUnauthorized
}
updateInput := user.UpdateUserInput{
ID: userID,
FirstName: input.FirstName,
LastName: input.LastName,
DisplayName: input.DisplayName,
Bio: input.Bio,
AvatarURL: input.AvatarURL,
}
if input.CountryID != nil {
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid country ID: %v", err)
}
uid := uint(countryID)
updateInput.CountryID = &uid
}
if input.CityID != nil {
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid city ID: %v", err)
}
uid := uint(cityID)
updateInput.CityID = &uid
}
if input.AddressID != nil {
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid address ID: %v", err)
}
uid := uint(addressID)
updateInput.AddressID = &uid
}
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
if err != nil {
return nil, err
}
return &model.User{
ID: fmt.Sprintf("%d", updatedUser.ID),
Username: updatedUser.Username,
Email: updatedUser.Email,
FirstName: &updatedUser.FirstName,
LastName: &updatedUser.LastName,
DisplayName: &updatedUser.DisplayName,
Bio: &updatedUser.Bio,
AvatarURL: &updatedUser.AvatarURL,
Role: model.UserRole(updatedUser.Role),
Verified: updatedUser.Verified,
Active: updatedUser.Active,
}, nil
}
// ChangePassword is the resolver for the changePassword field.
func (r *mutationResolver) ChangePassword(ctx context.Context, currentPassword string, newPassword string) (bool, error) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
changeInput := auth.ChangePasswordInput{
UserID: userID,
CurrentPassword: currentPassword,
NewPassword: newPassword,
}
err := r.App.Auth.Commands.ChangePassword(ctx, changeInput)
if err != nil {
return false, err
}
return true, nil
}
// 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)
}
workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, uint(workID))
if err != nil {
if errors.Is(err, domain.ErrEntityNotFound) {
return nil, nil
}
return nil, err
}
go func() {
if err := r.App.Analytics.IncrementWorkViews(context.Background(), uint(workID)); err != nil {
log.Error(err, "failed to increment work views")
}
}()
content := r.resolveWorkContent(ctx, workDTO.ID, workDTO.Language)
return &model.Work{
ID: id,
Name: workDTO.Title,
Language: workDTO.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) {
page := 1
pageSize := 20
if limit != nil {
pageSize = int(*limit)
}
if offset != nil {
page = int(*offset)/pageSize + 1
}
paginatedResult, err := r.App.Work.Queries.ListWorks(ctx, page, pageSize)
if err != nil {
return nil, err
}
var result []*model.Work
for _, w := range paginatedResult.Items {
content := r.resolveWorkContent(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) {
translationID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
translationDTO, err := r.App.Translation.Queries.Translation(ctx, uint(translationID))
if err != nil {
return nil, err
}
if translationDTO == nil {
return nil, nil
}
go func() {
if err := r.App.Analytics.IncrementTranslationViews(context.Background(), uint(translationID)); err != nil {
log.Error(err, "failed to increment translation views")
}
}()
return &model.Translation{
ID: id,
Name: translationDTO.Title,
Language: translationDTO.Language,
Content: &translationDTO.Content,
WorkID: fmt.Sprintf("%d", translationDTO.TranslatableID),
}, nil
}
// 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) {
wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
}
page := 1
pageSize := 20
if limit != nil {
pageSize = int(*limit)
}
if offset != nil {
page = int(*offset)/pageSize + 1
}
paginatedResult, err := r.App.Translation.Queries.ListTranslations(ctx, uint(wID), language, page, pageSize)
if err != nil {
return nil, err
}
var result []*model.Translation
for _, t := range paginatedResult.Items {
result = append(result, &model.Translation{
ID: fmt.Sprintf("%d", t.ID),
Name: t.Title,
Language: t.Language,
Content: &t.Content,
WorkID: workID,
})
}
return result, nil
}
// Book is the resolver for the book field.
func (r *queryResolver) Book(ctx context.Context, id string) (*model.Book, error) {
bookID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
}
bookRecord, err := r.App.Book.Queries.Book(ctx, uint(bookID))
if err != nil {
return nil, err
}
if bookRecord == nil {
return nil, nil
}
return &model.Book{
ID: fmt.Sprintf("%d", bookRecord.ID),
Name: bookRecord.Title,
Language: bookRecord.Language,
Description: &bookRecord.Description,
Isbn: &bookRecord.ISBN,
}, nil
}
// Books is the resolver for the books field.
func (r *queryResolver) Books(ctx context.Context, limit *int32, offset *int32) ([]*model.Book, error) {
books, err := r.App.Book.Queries.Books(ctx)
if err != nil {
return nil, err
}
var result []*model.Book
for _, b := range books {
result = append(result, &model.Book{
ID: fmt.Sprintf("%d", b.ID),
Name: b.Title,
Language: b.Language,
Description: &b.Description,
Isbn: &b.ISBN,
})
}
return result, nil
}
// Author is the resolver for the author field.
func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, error) {
authorID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid author ID", domain.ErrValidation)
}
authorRecord, err := r.App.Author.Queries.Author(ctx, uint(authorID))
if err != nil {
return nil, err
}
if authorRecord == nil {
return nil, nil
}
var bio *string
authorWithTranslations, err := r.App.Author.Queries.AuthorWithTranslations(ctx, authorRecord.ID)
if err == nil && authorWithTranslations != nil {
biography, err := r.App.Localization.Queries.GetAuthorBiography(ctx, authorRecord.ID, authorRecord.Language)
if err == nil && biography != "" {
bio = &biography
}
}
return &model.Author{
ID: fmt.Sprintf("%d", authorRecord.ID),
Name: authorRecord.Name,
Language: authorRecord.Language,
Biography: bio,
}, nil
}
// 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
var countryIDUint *uint
if countryID != nil {
parsedID, err := strconv.ParseUint(*countryID, 10, 32)
if err != nil {
return nil, err
}
uid := uint(parsedID)
countryIDUint = &uid
}
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUint)
if err != nil {
return nil, err
}
var result []*model.Author
for _, a := range authors {
var bio *string
authorWithTranslations, err := r.App.Author.Queries.AuthorWithTranslations(ctx, a.ID)
if err == nil && authorWithTranslations != nil {
biography, err := r.App.Localization.Queries.GetAuthorBiography(ctx, a.ID, a.Language)
if err == nil && biography != "" {
bio = &biography
}
}
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) {
userID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
}
userRecord, err := r.App.User.Queries.User(ctx, uint(userID))
if err != nil {
return nil, err
}
if userRecord == nil {
return nil, nil
}
return &model.User{
ID: fmt.Sprintf("%d", userRecord.ID),
Username: userRecord.Username,
Email: userRecord.Email,
FirstName: &userRecord.FirstName,
LastName: &userRecord.LastName,
DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL,
Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified,
Active: userRecord.Active,
}, nil
}
// UserByEmail is the resolver for the userByEmail field.
func (r *queryResolver) UserByEmail(ctx context.Context, email string) (*model.User, error) {
userRecord, err := r.App.User.Queries.UserByEmail(ctx, email)
if err != nil {
return nil, err
}
if userRecord == nil {
return nil, nil
}
return &model.User{
ID: fmt.Sprintf("%d", userRecord.ID),
Username: userRecord.Username,
Email: userRecord.Email,
FirstName: &userRecord.FirstName,
LastName: &userRecord.LastName,
DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL,
Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified,
Active: userRecord.Active,
}, nil
}
// UserByUsername is the resolver for the userByUsername field.
func (r *queryResolver) UserByUsername(ctx context.Context, username string) (*model.User, error) {
userRecord, err := r.App.User.Queries.UserByUsername(ctx, username)
if err != nil {
return nil, err
}
if userRecord == nil {
return nil, nil
}
return &model.User{
ID: fmt.Sprintf("%d", userRecord.ID),
Username: userRecord.Username,
Email: userRecord.Email,
FirstName: &userRecord.FirstName,
LastName: &userRecord.LastName,
DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL,
Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified,
Active: userRecord.Active,
}, nil
}
// 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 {
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.User.Queries.UsersByRole(ctx, modelRole)
} else {
users, err = r.App.User.Queries.Users(ctx)
}
if err != nil {
return nil, err
}
var result []*model.User
for _, u := range users {
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) {
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, domain.ErrUnauthorized
}
userRecord, err := r.App.User.Queries.User(ctx, userID)
if err != nil {
return nil, err
}
if userRecord == nil {
return nil, domain.ErrUserNotFound
}
return &model.User{
ID: fmt.Sprintf("%d", userRecord.ID),
Username: userRecord.Username,
Email: userRecord.Email,
FirstName: &userRecord.FirstName,
LastName: &userRecord.LastName,
DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL,
Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified,
Active: userRecord.Active,
}, nil
}
// UserProfile is the resolver for the userProfile field.
func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) {
uID, err := strconv.ParseUint(userID, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
}
profile, err := r.App.User.Queries.UserProfile(ctx, uint(uID))
if err != nil {
return nil, err
}
if profile == nil {
return nil, nil
}
user, err := r.App.User.Queries.User(ctx, uint(uID))
if err != nil {
return nil, err
}
if user == nil {
return nil, fmt.Errorf("user not found for profile %d", profile.ID)
}
return &model.UserProfile{
ID: fmt.Sprintf("%d", profile.ID),
UserID: userID,
User: &model.User{
ID: fmt.Sprintf("%d", user.ID),
Username: user.Username,
Email: user.Email,
FirstName: &user.FirstName,
LastName: &user.LastName,
DisplayName: &user.DisplayName,
Bio: &user.Bio,
AvatarURL: &user.AvatarURL,
Role: model.UserRole(user.Role),
Verified: user.Verified,
Active: user.Active,
},
PhoneNumber: &profile.PhoneNumber,
Website: &profile.Website,
Twitter: &profile.Twitter,
Facebook: &profile.Facebook,
LinkedIn: &profile.LinkedIn,
Github: &profile.Github,
}, nil
}
// Collection is the resolver for the collection field.
func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Collection, error) {
collID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid collection ID", domain.ErrValidation)
}
collectionRecord, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
if err != nil {
return nil, err
}
if collectionRecord == nil {
return nil, nil
}
workRecords, err := r.App.Work.Queries.ListByCollectionID(ctx, uint(collID))
if err != nil {
return nil, err
}
var works []*model.Work
for _, w := range workRecords {
content := r.resolveWorkContent(ctx, w.ID, w.Language)
works = append(works, &model.Work{
ID: fmt.Sprintf("%d", w.ID),
Name: w.Title,
Language: w.Language,
Content: content,
})
}
return &model.Collection{
ID: fmt.Sprintf("%d", collectionRecord.ID),
Name: collectionRecord.Name,
Description: &collectionRecord.Description,
Works: works,
User: &model.User{
ID: fmt.Sprintf("%d", collectionRecord.UserID),
},
}, nil
}
// Collections is the resolver for the collections field.
func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *int32, offset *int32) ([]*model.Collection, error) {
var collectionRecords []domain.Collection
var err error
if userID != nil {
uID, idErr := strconv.ParseUint(*userID, 10, 32)
if idErr != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
}
collectionRecords, err = r.App.Collection.Queries.CollectionsByUserID(ctx, uint(uID))
} else {
collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx)
}
if err != nil {
return nil, err
}
start := 0
if offset != nil {
start = int(*offset)
}
end := len(collectionRecords)
if limit != nil {
end = start + int(*limit)
if end > len(collectionRecords) {
end = len(collectionRecords)
}
}
if start > len(collectionRecords) {
start = len(collectionRecords)
}
paginatedRecords := collectionRecords[start:end]
var result []*model.Collection
for _, c := range paginatedRecords {
result = append(result, &model.Collection{
ID: fmt.Sprintf("%d", c.ID),
Name: c.Name,
Description: &c.Description,
User: &model.User{
ID: fmt.Sprintf("%d", c.UserID),
},
})
}
return result, nil
}
// 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.Tag.Queries.Tag(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) {
tags, err := r.App.Tag.Queries.Tags(ctx)
if err != nil {
return nil, err
}
var result []*model.Tag
for _, t := range tags {
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, fmt.Errorf("invalid category ID: %v", err)
}
category, err := r.App.Category.Queries.Category(ctx, uint(categoryID))
if err != nil {
return nil, err
}
if category == nil {
return nil, nil
}
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) {
categories, err := r.App.Category.Queries.Categories(ctx)
if err != nil {
return nil, err
}
var result []*model.Category
for _, c := range categories {
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) {
cID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid comment ID", domain.ErrValidation)
}
commentRecord, err := r.App.Comment.Queries.Comment(ctx, uint(cID))
if err != nil {
return nil, err
}
if commentRecord == nil {
return nil, nil
}
return &model.Comment{
ID: fmt.Sprintf("%d", commentRecord.ID),
Text: commentRecord.Text,
User: &model.User{
ID: fmt.Sprintf("%d", commentRecord.UserID),
},
}, nil
}
// 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) {
var commentRecords []domain.Comment
var err error
if workID != nil {
wID, idErr := strconv.ParseUint(*workID, 10, 32)
if idErr != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
}
commentRecords, err = r.App.Comment.Queries.CommentsByWorkID(ctx, uint(wID))
} else if translationID != nil {
tID, idErr := strconv.ParseUint(*translationID, 10, 32)
if idErr != nil {
return nil, fmt.Errorf("%w: invalid translation ID", domain.ErrValidation)
}
commentRecords, err = r.App.Comment.Queries.CommentsByTranslationID(ctx, uint(tID))
} else if userID != nil {
uID, idErr := strconv.ParseUint(*userID, 10, 32)
if idErr != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
}
commentRecords, err = r.App.Comment.Queries.CommentsByUserID(ctx, uint(uID))
} else {
commentRecords, err = r.App.Comment.Queries.Comments(ctx)
}
if err != nil {
return nil, err
}
start := 0
if offset != nil {
start = int(*offset)
}
end := len(commentRecords)
if limit != nil {
end = start + int(*limit)
if end > len(commentRecords) {
end = len(commentRecords)
}
}
if start > len(commentRecords) {
start = len(commentRecords)
}
paginatedRecords := commentRecords[start:end]
var result []*model.Comment
for _, c := range paginatedRecords {
result = append(result, &model.Comment{
ID: fmt.Sprintf("%d", c.ID),
Text: c.Text,
User: &model.User{
ID: fmt.Sprintf("%d", c.UserID),
},
})
}
return result, nil
}
// 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) {
page := 1
pageSize := 20
if limit != nil {
pageSize = int(*limit)
}
if offset != nil {
page = int(*offset)/pageSize + 1
}
var searchFilters domain.SearchFilters
if filters != nil {
searchFilters.Languages = filters.Languages
searchFilters.Categories = filters.Categories
searchFilters.Tags = filters.Tags
searchFilters.Authors = filters.Authors
if filters.DateFrom != nil {
t, err := time.Parse(time.RFC3339, *filters.DateFrom)
if err != nil {
return nil, fmt.Errorf("invalid DateFrom format: %w", err)
}
searchFilters.DateFrom = &t
}
if filters.DateTo != nil {
t, err := time.Parse(time.RFC3339, *filters.DateTo)
if err != nil {
return nil, fmt.Errorf("invalid DateTo format: %w", err)
}
searchFilters.DateTo = &t
}
}
results, err := r.App.Search.Search(ctx, query, page, pageSize, searchFilters)
if err != nil {
return nil, err
}
var works []*model.Work
for _, w := range results.Works {
works = append(works, &model.Work{
ID: fmt.Sprintf("%d", w.ID),
Name: w.Title,
Language: w.Language,
})
}
var translations []*model.Translation
for _, t := range results.Translations {
translations = append(translations, &model.Translation{
ID: fmt.Sprintf("%d", t.ID),
Name: t.Title,
Language: t.Language,
Content: &t.Content,
WorkID: fmt.Sprintf("%d", t.TranslatableID),
})
}
var authors []*model.Author
for _, a := range results.Authors {
authors = append(authors, &model.Author{
ID: fmt.Sprintf("%d", a.ID),
Name: a.Name,
Language: a.Language,
})
}
return &model.SearchResults{
Works: works,
Translations: translations,
Authors: authors,
Total: int32(results.Total),
}, nil
}
// TrendingWorks is the resolver for the trendingWorks field.
func (r *queryResolver) TrendingWorks(ctx context.Context, timePeriod *string, limit *int32) ([]*model.Work, error) {
tp := "daily"
if timePeriod != nil {
tp = *timePeriod
}
l := 10
if limit != nil {
l = int(*limit)
}
workRecords, err := r.App.Analytics.GetTrendingWorks(ctx, tp, l)
if err != nil {
return nil, err
}
var result []*model.Work
for _, w := range workRecords {
result = append(result, &model.Work{
ID: fmt.Sprintf("%d", w.ID),
Name: w.Title,
Language: w.Language,
})
}
return result, nil
}
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }