mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
This commit refactors the GraphQL layer to improve code quality and adhere to the project's target architecture.
Key changes include: - Moved authorization logic for collection mutations from the GraphQL resolvers to the application service layer, ensuring that ownership checks are handled consistently within the business logic. - Updated the `collection` command handlers and input structs to accept a user ID for authorization. - Removed orphaned code, including unused resolver definitions (`workResolver`, `translationResolver`) and misplaced helper functions from `schema.resolvers.go`. - Re-implemented the `Stats` resolvers for the `Work` and `Translation` types, ensuring they correctly call the `analytics` application service. - Fixed several build errors related to type mismatches and redeclared functions by regenerating the GraphQL code and correcting helper function signatures. - Updated integration tests to provide authenticated user context for collection mutations, ensuring that the new authorization checks pass.
This commit is contained in:
parent
02b06fd9ce
commit
8ddc4a7986
@ -34,3 +34,12 @@ func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uint, pre
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toInt32(i int64) *int32 {
|
||||||
|
val := int32(i)
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt(i int) *int {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|||||||
@ -1048,7 +1048,7 @@ func (s *GraphQLIntegrationSuite) TestTrendingWorksQuery() {
|
|||||||
|
|
||||||
func (s *GraphQLIntegrationSuite) TestCollectionMutations() {
|
func (s *GraphQLIntegrationSuite) TestCollectionMutations() {
|
||||||
// Create users for testing authorization
|
// Create users for testing authorization
|
||||||
_, ownerToken := s.CreateAuthenticatedUser("collectionowner", "owner@test.com", domain.UserRoleReader)
|
owner, ownerToken := s.CreateAuthenticatedUser("collectionowner", "owner@test.com", domain.UserRoleReader)
|
||||||
otherUser, otherToken := s.CreateAuthenticatedUser("otheruser", "other@test.com", domain.UserRoleReader)
|
otherUser, otherToken := s.CreateAuthenticatedUser("otheruser", "other@test.com", domain.UserRoleReader)
|
||||||
_ = otherUser
|
_ = otherUser
|
||||||
|
|
||||||
@ -1175,6 +1175,7 @@ func (s *GraphQLIntegrationSuite) TestCollectionMutations() {
|
|||||||
err = s.App.Collection.Commands.AddWorkToCollection(context.Background(), collection.AddWorkToCollectionInput{
|
err = s.App.Collection.Commands.AddWorkToCollection(context.Background(), collection.AddWorkToCollectionInput{
|
||||||
CollectionID: uint(collectionIDInt),
|
CollectionID: uint(collectionIDInt),
|
||||||
WorkID: work.ID,
|
WorkID: work.ID,
|
||||||
|
UserID: owner.ID,
|
||||||
})
|
})
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
|||||||
@ -395,24 +395,11 @@ func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, inpu
|
|||||||
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the existing collection
|
|
||||||
collectionModel, err := r.App.Collection.Queries.Collection(ctx, uint(collectionID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if collectionModel == nil {
|
|
||||||
return nil, fmt.Errorf("collection not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check ownership
|
|
||||||
if collectionModel.UserID != userID {
|
|
||||||
return nil, fmt.Errorf("unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call collection service
|
// Call collection service
|
||||||
updateInput := collection.UpdateCollectionInput{
|
updateInput := collection.UpdateCollectionInput{
|
||||||
ID: uint(collectionID),
|
ID: uint(collectionID),
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
|
UserID: userID,
|
||||||
}
|
}
|
||||||
if input.Description != nil {
|
if input.Description != nil {
|
||||||
updateInput.Description = *input.Description
|
updateInput.Description = *input.Description
|
||||||
@ -447,22 +434,8 @@ func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (boo
|
|||||||
return false, fmt.Errorf("invalid collection ID: %v", err)
|
return false, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the existing collection
|
// Call collection service
|
||||||
collection, err := r.App.Collection.Queries.Collection(ctx, uint(collectionID))
|
err = r.App.Collection.Commands.DeleteCollection(ctx, uint(collectionID), userID)
|
||||||
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.Collection.Commands.DeleteCollection(ctx, uint(collectionID))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -488,24 +461,11 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
|
|||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the existing collection
|
|
||||||
collectionModel, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if collectionModel == nil {
|
|
||||||
return nil, fmt.Errorf("collection not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check ownership
|
|
||||||
if collectionModel.UserID != userID {
|
|
||||||
return nil, fmt.Errorf("unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add work to collection
|
// Add work to collection
|
||||||
addInput := collection.AddWorkToCollectionInput{
|
addInput := collection.AddWorkToCollectionInput{
|
||||||
CollectionID: uint(collID),
|
CollectionID: uint(collID),
|
||||||
WorkID: uint(wID),
|
WorkID: uint(wID),
|
||||||
|
UserID: userID,
|
||||||
}
|
}
|
||||||
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
|
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -544,24 +504,11 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
|||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the existing collection
|
|
||||||
collectionModel, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if collectionModel == nil {
|
|
||||||
return nil, fmt.Errorf("collection not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check ownership
|
|
||||||
if collectionModel.UserID != userID {
|
|
||||||
return nil, fmt.Errorf("unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove work from collection
|
// Remove work from collection
|
||||||
removeInput := collection.RemoveWorkFromCollectionInput{
|
removeInput := collection.RemoveWorkFromCollectionInput{
|
||||||
CollectionID: uint(collID),
|
CollectionID: uint(collID),
|
||||||
WorkID: uint(wID),
|
WorkID: uint(wID),
|
||||||
|
UserID: userID,
|
||||||
}
|
}
|
||||||
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
|
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1325,16 +1272,27 @@ type queryResolver struct{ *Resolver }
|
|||||||
// it when you're done.
|
// it when you're done.
|
||||||
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
||||||
/*
|
/*
|
||||||
func (r *Resolver) Work() WorkResolver { return &workResolver{r} }
|
func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) {
|
||||||
func (r *Resolver) Translation() TranslationResolver { return &translationResolver{r} }
|
translationID, err := strconv.ParseUint(obj.ID, 10, 32)
|
||||||
type workResolver struct{ *Resolver }
|
if err != nil {
|
||||||
type translationResolver struct{ *Resolver }
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
func toInt32(i int64) *int {
|
}
|
||||||
val := int(i)
|
|
||||||
return &val
|
stats, err := r.App.Analytics.GetOrCreateTranslationStats(ctx, uint(translationID))
|
||||||
}
|
if err != nil {
|
||||||
func toInt(i int) *int {
|
return nil, err
|
||||||
return &i
|
}
|
||||||
|
|
||||||
|
// Convert domain model to GraphQL model
|
||||||
|
return &model.TranslationStats{
|
||||||
|
ID: fmt.Sprintf("%d", stats.ID),
|
||||||
|
Views: toInt32(stats.Views),
|
||||||
|
Likes: toInt32(stats.Likes),
|
||||||
|
Comments: toInt32(stats.Comments),
|
||||||
|
Shares: toInt32(stats.Shares),
|
||||||
|
ReadingTime: toInt32(int64(stats.ReadingTime)),
|
||||||
|
Sentiment: &stats.Sentiment,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkStats, error) {
|
func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkStats, error) {
|
||||||
workID, err := strconv.ParseUint(obj.ID, 10, 32)
|
workID, err := strconv.ParseUint(obj.ID, 10, 32)
|
||||||
@ -1356,31 +1314,13 @@ func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkS
|
|||||||
Bookmarks: toInt32(stats.Bookmarks),
|
Bookmarks: toInt32(stats.Bookmarks),
|
||||||
Shares: toInt32(stats.Shares),
|
Shares: toInt32(stats.Shares),
|
||||||
TranslationCount: toInt32(stats.TranslationCount),
|
TranslationCount: toInt32(stats.TranslationCount),
|
||||||
ReadingTime: toInt(stats.ReadingTime),
|
ReadingTime: toInt32(int64(stats.ReadingTime)),
|
||||||
Complexity: &stats.Complexity,
|
Complexity: &stats.Complexity,
|
||||||
Sentiment: &stats.Sentiment,
|
Sentiment: &stats.Sentiment,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) {
|
func (r *Resolver) Translation() TranslationResolver { return &translationResolver{r} }
|
||||||
translationID, err := strconv.ParseUint(obj.ID, 10, 32)
|
func (r *Resolver) Work() WorkResolver { return &workResolver{r} }
|
||||||
if err != nil {
|
type translationResolver struct{ *Resolver }
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
type workResolver struct{ *Resolver }
|
||||||
}
|
|
||||||
|
|
||||||
stats, err := r.App.Analytics.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: toInt32(stats.Views),
|
|
||||||
Likes: toInt32(stats.Likes),
|
|
||||||
Comments: toInt32(stats.Comments),
|
|
||||||
Shares: toInt32(stats.Shares),
|
|
||||||
ReadingTime: toInt(stats.ReadingTime),
|
|
||||||
Sentiment: &stats.Sentiment,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package collection
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ type UpdateCollectionInput struct {
|
|||||||
Description string
|
Description string
|
||||||
IsPublic bool
|
IsPublic bool
|
||||||
CoverImageURL string
|
CoverImageURL string
|
||||||
|
UserID uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCollection updates an existing collection.
|
// UpdateCollection updates an existing collection.
|
||||||
@ -55,6 +57,9 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if collection.UserID != input.UserID {
|
||||||
|
return nil, fmt.Errorf("unauthorized: user %d cannot update collection %d", input.UserID, input.ID)
|
||||||
|
}
|
||||||
collection.Name = input.Name
|
collection.Name = input.Name
|
||||||
collection.Description = input.Description
|
collection.Description = input.Description
|
||||||
collection.IsPublic = input.IsPublic
|
collection.IsPublic = input.IsPublic
|
||||||
@ -67,7 +72,14 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCollection deletes a collection by ID.
|
// DeleteCollection deletes a collection by ID.
|
||||||
func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint) error {
|
func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint, userID uint) error {
|
||||||
|
collection, err := c.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if collection.UserID != userID {
|
||||||
|
return fmt.Errorf("unauthorized: user %d cannot delete collection %d", userID, id)
|
||||||
|
}
|
||||||
return c.repo.Delete(ctx, id)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,10 +87,18 @@ func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint) erro
|
|||||||
type AddWorkToCollectionInput struct {
|
type AddWorkToCollectionInput struct {
|
||||||
CollectionID uint
|
CollectionID uint
|
||||||
WorkID uint
|
WorkID uint
|
||||||
|
UserID uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddWorkToCollection adds a work to a collection.
|
// AddWorkToCollection adds a work to a collection.
|
||||||
func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddWorkToCollectionInput) error {
|
func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddWorkToCollectionInput) error {
|
||||||
|
collection, err := c.repo.GetByID(ctx, input.CollectionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if collection.UserID != input.UserID {
|
||||||
|
return fmt.Errorf("unauthorized: user %d cannot add work to collection %d", input.UserID, input.CollectionID)
|
||||||
|
}
|
||||||
return c.repo.AddWorkToCollection(ctx, input.CollectionID, input.WorkID)
|
return c.repo.AddWorkToCollection(ctx, input.CollectionID, input.WorkID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,9 +106,17 @@ func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddW
|
|||||||
type RemoveWorkFromCollectionInput struct {
|
type RemoveWorkFromCollectionInput struct {
|
||||||
CollectionID uint
|
CollectionID uint
|
||||||
WorkID uint
|
WorkID uint
|
||||||
|
UserID uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveWorkFromCollection removes a work from a collection.
|
// RemoveWorkFromCollection removes a work from a collection.
|
||||||
func (c *CollectionCommands) RemoveWorkFromCollection(ctx context.Context, input RemoveWorkFromCollectionInput) error {
|
func (c *CollectionCommands) RemoveWorkFromCollection(ctx context.Context, input RemoveWorkFromCollectionInput) error {
|
||||||
|
collection, err := c.repo.GetByID(ctx, input.CollectionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if collection.UserID != input.UserID {
|
||||||
|
return fmt.Errorf("unauthorized: user %d cannot remove work from collection %d", input.UserID, input.CollectionID)
|
||||||
|
}
|
||||||
return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID)
|
return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user