package graph import ( "context" "fmt" "io" "log" "tailly_back_v2/internal/domain" "tailly_back_v2/proto" "time" "github.com/99designs/gqlgen/graphql" ) // IsLiked is the resolver for the isLiked field. func (r *clipResolver) IsLiked(ctx context.Context, obj *domain.Clip) (bool, error) { userID, err := getUserIDFromContext(ctx) if err != nil { return false, fmt.Errorf("user not authenticated: %w", err) } resp, err := r.ClipClient.CheckIfLiked(ctx, &proto.CheckIfLikedRequest{ ClipId: int32(obj.ID), UserId: int32(userID), }) if err != nil { return false, fmt.Errorf("failed to check like status: %w", err) } return resp.IsLiked, nil } func (r *clipResolver) Author(ctx context.Context, obj *domain.Clip) (*domain.User, error) { if obj.AuthorID == 0 { log.Printf("ERROR: Clip %d has no author ID", obj.ID) return &domain.User{ ID: 0, Username: "Unknown", Avatar: "/img/logo.png", }, nil // Не возвращаем ошибку, чтобы не ломать весь запрос } author, err := r.Services.User.GetByID(ctx, obj.AuthorID) if err != nil { log.Printf("ERROR: Failed to get author %d for clip %d: %v", obj.AuthorID, obj.ID, err) // Возвращаем заглушку вместо ошибки return &domain.User{ ID: obj.AuthorID, Username: "Unknown", Avatar: "/img/logo.png", }, nil } if author == nil { log.Printf("ERROR: Author %d not found for clip %d", obj.AuthorID, obj.ID) return &domain.User{ ID: obj.AuthorID, Username: "Deleted User", Avatar: "/img/logo.png", }, nil } log.Printf("Successfully resolved author %d for clip %d", obj.AuthorID, obj.ID) log.Printf("Successfully resolved author %d for clip %d", author.Avatar, author.ID, author.Username) return author, nil } // CreatedAt is the resolver for the createdAt field. func (r *clipResolver) CreatedAt(ctx context.Context, obj *domain.Clip) (string, error) { return obj.CreatedAt.Format(time.RFC3339), nil } // UpdatedAt is the resolver for the updatedAt field. func (r *clipResolver) UpdatedAt(ctx context.Context, obj *domain.Clip) (string, error) { return obj.UpdatedAt.Format(time.RFC3339), nil } // CreatedAt is the resolver for the createdAt field. func (r *clipCommentResolver) CreatedAt(ctx context.Context, obj *domain.ClipComment) (string, error) { return obj.CreatedAt.Format(time.RFC3339), nil } // UpdatedAt is the resolver for the updatedAt field. func (r *clipCommentResolver) UpdatedAt(ctx context.Context, obj *domain.ClipComment) (string, error) { return obj.UpdatedAt.Format(time.RFC3339), nil } // CreatedAt is the resolver for the createdAt field. func (r *clipLikeResolver) CreatedAt(ctx context.Context, obj *domain.ClipLike) (string, error) { return obj.CreatedAt.Format(time.RFC3339), nil } // CreateClip is the resolver for the createClip field. func (r *mutationResolver) CreateClip(ctx context.Context, title *string, video graphql.Upload) (*domain.Clip, error) { userID, err := getUserIDFromContext(ctx) if err != nil { return nil, fmt.Errorf("user not authenticated: %w", err) } var clipTitle string if title != nil { clipTitle = *title } // Читаем данные видео videoData, err := io.ReadAll(video.File) if err != nil { return nil, fmt.Errorf("failed to read video data: %w", err) } resp, err := r.ClipClient.CreateClip(ctx, &proto.CreateClipRequest{ UserId: int32(userID), Title: clipTitle, VideoData: videoData, FileName: video.Filename, ContentType: video.ContentType, }) if err != nil { return nil, fmt.Errorf("failed to create clip: %w", err) } return r.protoClipToDomain(resp.Clip), nil } // DeleteClip is the resolver for the deleteClip field. func (r *mutationResolver) DeleteClip(ctx context.Context, id int) (bool, error) { userID, err := getUserIDFromContext(ctx) if err != nil { return false, fmt.Errorf("user not authenticated: %w", err) } _, err = r.ClipClient.DeleteClip(ctx, &proto.DeleteClipRequest{ ClipId: int32(id), UserId: int32(userID), }) if err != nil { return false, fmt.Errorf("failed to delete clip: %w", err) } return true, nil } // LikeClip is the resolver for the likeClip field. func (r *mutationResolver) LikeClip(ctx context.Context, clipID int) (*domain.ClipLike, error) { userID, err := getUserIDFromContext(ctx) if err != nil { return nil, fmt.Errorf("user not authenticated: %w", err) } resp, err := r.ClipClient.LikeClip(ctx, &proto.LikeClipRequest{ ClipId: int32(clipID), UserId: int32(userID), }) if err != nil { return nil, fmt.Errorf("failed to like clip: %w", err) } return r.protoLikeToDomain(resp.Like), nil } // UnlikeClip is the resolver for the unlikeClip field. func (r *mutationResolver) UnlikeClip(ctx context.Context, clipID int) (bool, error) { userID, err := getUserIDFromContext(ctx) if err != nil { return false, fmt.Errorf("user not authenticated: %w", err) } _, err = r.ClipClient.UnlikeClip(ctx, &proto.UnlikeClipRequest{ ClipId: int32(clipID), UserId: int32(userID), }) if err != nil { return false, fmt.Errorf("failed to unlike clip: %w", err) } return true, nil } // CreateClipComment is the resolver for the createClipComment field. func (r *mutationResolver) CreateClipComment(ctx context.Context, clipID int, content string) (*domain.ClipComment, error) { userID, err := getUserIDFromContext(ctx) if err != nil { return nil, fmt.Errorf("user not authenticated: %w", err) } resp, err := r.ClipClient.CreateComment(ctx, &proto.CreateCommentRequest{ ClipId: int32(clipID), UserId: int32(userID), Content: content, }) if err != nil { return nil, fmt.Errorf("failed to create comment: %w", err) } return r.protoCommentToDomain(resp.Comment), nil } // DeleteClipComment is the resolver for the deleteClipComment field. func (r *mutationResolver) DeleteClipComment(ctx context.Context, commentID int) (bool, error) { userID, err := getUserIDFromContext(ctx) if err != nil { return false, fmt.Errorf("user not authenticated: %w", err) } _, err = r.ClipClient.DeleteComment(ctx, &proto.DeleteCommentRequest{ CommentId: int32(commentID), UserId: int32(userID), }) if err != nil { return false, fmt.Errorf("failed to delete comment: %w", err) } return true, nil } // Clip is the resolver for the clip field. func (r *queryResolver) Clip(ctx context.Context, id int) (*domain.Clip, error) { resp, err := r.ClipClient.GetClip(ctx, &proto.GetClipRequest{ ClipId: int32(id), }) if err != nil { return nil, fmt.Errorf("failed to get clip: %w", err) } return r.protoClipToDomain(resp.Clip), nil } // Clips is the resolver for the clips field. func (r *queryResolver) Clips(ctx context.Context, limit *int, offset *int) ([]*domain.Clip, error) { limitVal := 20 if limit != nil { limitVal = *limit } offsetVal := 0 if offset != nil { offsetVal = *offset } resp, err := r.ClipClient.GetClips(ctx, &proto.GetClipsRequest{ Limit: int32(limitVal), Offset: int32(offsetVal), }) if err != nil { log.Printf("ERROR: Failed to get clips from gRPC service: %v", err) return nil, fmt.Errorf("failed to get clips: %w", err) } clips := r.protoClipsToDomain(resp.Clips) // Добавьте логирование для дебага log.Printf("Retrieved %d clips from gRPC service", len(clips)) for i, clip := range clips { log.Printf("Clip %d: ID=%d, Title=%s, VideoURL=%s, AuthorID=%d", i, clip.ID, clip.Title, clip.VideoURL, clip.AuthorID) } return clips, nil } // UserClips is the resolver for the userClips field. func (r *queryResolver) UserClips(ctx context.Context, userID int, limit *int, offset *int) ([]*domain.Clip, error) { limitVal := 20 if limit != nil { limitVal = *limit } offsetVal := 0 if offset != nil { offsetVal = *offset } resp, err := r.ClipClient.GetUserClips(ctx, &proto.GetUserClipsRequest{ UserId: int32(userID), Limit: int32(limitVal), Offset: int32(offsetVal), }) if err != nil { return nil, fmt.Errorf("failed to get user clips: %w", err) } return r.protoClipsToDomain(resp.Clips), nil } // ClipComments is the resolver for the clipComments field. func (r *queryResolver) ClipComments(ctx context.Context, clipID int, limit *int, offset *int) ([]*domain.ClipComment, error) { limitVal := 20 if limit != nil { limitVal = *limit } offsetVal := 0 if offset != nil { offsetVal = *offset } resp, err := r.ClipClient.GetClipComments(ctx, &proto.GetClipCommentsRequest{ ClipId: int32(clipID), Limit: int32(limitVal), Offset: int32(offsetVal), }) if err != nil { return nil, fmt.Errorf("failed to get clip comments: %w", err) } return r.protoCommentsToDomain(resp.Comments), nil } // ClipLikes is the resolver for the clipLikes field. func (r *queryResolver) ClipLikes(ctx context.Context, clipID int, limit *int, offset *int) ([]*domain.ClipLike, error) { limitVal := 20 if limit != nil { limitVal = *limit } offsetVal := 0 if offset != nil { offsetVal = *offset } resp, err := r.ClipClient.GetClipLikes(ctx, &proto.GetClipLikesRequest{ ClipId: int32(clipID), Limit: int32(limitVal), Offset: int32(offsetVal), }) if err != nil { return nil, fmt.Errorf("failed to get clip likes: %w", err) } return r.protoLikesToDomain(resp.Likes), nil } // IsLiked is the resolver for the isLiked field. func (r *queryResolver) IsLiked(ctx context.Context, clipID int) (bool, error) { userID, err := getUserIDFromContext(ctx) if err != nil { return false, fmt.Errorf("user not authenticated: %w", err) } resp, err := r.ClipClient.CheckIfLiked(ctx, &proto.CheckIfLikedRequest{ ClipId: int32(clipID), UserId: int32(userID), }) if err != nil { return false, fmt.Errorf("failed to check like status: %w", err) } return resp.IsLiked, nil } // ClipCreated is the resolver for the clipCreated field. func (r *subscriptionResolver) ClipCreated(ctx context.Context) (<-chan *domain.Clip, error) { // Реализация подписки требует дополнительной инфраструктуры (Redis, Kafka и т.д.) // Возвращаем ошибку, так как это не реализовано в gRPC сервисе return nil, fmt.Errorf("subscriptions not implemented") } // ClipLiked is the resolver for the clipLiked field. func (r *subscriptionResolver) ClipLiked(ctx context.Context, clipID int) (<-chan *domain.ClipLike, error) { // Реализация подписки требует дополнительной инфраструктуры return nil, fmt.Errorf("subscriptions not implemented") } // CommentClipCreated is the resolver for the commentClipCreated field. func (r *subscriptionResolver) CommentClipCreated(ctx context.Context, clipID int) (<-chan *domain.ClipComment, error) { // Реализация подписки требует дополнительной инфраструктуры return nil, fmt.Errorf("subscriptions not implemented") } // Вспомогательные методы для конвертации func (r *Resolver) protoClipToDomain(protoClip *proto.Clip) *domain.Clip { if protoClip == nil { return nil } return &domain.Clip{ ID: int(protoClip.Id), Title: protoClip.Title, VideoURL: protoClip.VideoUrl, ThumbnailURL: protoClip.ThumbnailUrl, AuthorID: int(protoClip.AuthorId), LikesCount: int(protoClip.LikesCount), CommentsCount: int(protoClip.CommentsCount), CreatedAt: protoClip.CreatedAt.AsTime(), UpdatedAt: protoClip.UpdatedAt.AsTime(), } } func (r *Resolver) protoClipsToDomain(protoClips []*proto.Clip) []*domain.Clip { var clips []*domain.Clip for _, protoClip := range protoClips { clips = append(clips, r.protoClipToDomain(protoClip)) } return clips } func (r *Resolver) protoLikeToDomain(protoLike *proto.ClipLike) *domain.ClipLike { if protoLike == nil { return nil } return &domain.ClipLike{ ID: int(protoLike.Id), ClipID: int(protoLike.ClipId), UserID: int(protoLike.UserId), CreatedAt: protoLike.CreatedAt.AsTime(), } } func (r *Resolver) protoLikesToDomain(protoLikes []*proto.ClipLike) []*domain.ClipLike { var likes []*domain.ClipLike for _, protoLike := range protoLikes { likes = append(likes, r.protoLikeToDomain(protoLike)) } return likes } func (r *Resolver) protoCommentToDomain(protoComment *proto.ClipComment) *domain.ClipComment { if protoComment == nil { return nil } return &domain.ClipComment{ ID: int(protoComment.Id), ClipID: int(protoComment.ClipId), AuthorID: int(protoComment.AuthorId), Content: protoComment.Content, CreatedAt: protoComment.CreatedAt.AsTime(), UpdatedAt: protoComment.UpdatedAt.AsTime(), } } func (r *Resolver) protoCommentsToDomain(protoComments []*proto.ClipComment) []*domain.ClipComment { var comments []*domain.ClipComment for _, protoComment := range protoComments { comments = append(comments, r.protoCommentToDomain(protoComment)) } return comments } // Clip returns ClipResolver implementation. func (r *Resolver) Clip() ClipResolver { return &clipResolver{r} } // ClipComment returns ClipCommentResolver implementation. func (r *Resolver) ClipComment() ClipCommentResolver { return &clipCommentResolver{r} } // ClipLike returns ClipLikeResolver implementation. func (r *Resolver) ClipLike() ClipLikeResolver { return &clipLikeResolver{r} } type clipResolver struct{ *Resolver } type clipCommentResolver struct{ *Resolver } type clipLikeResolver struct{ *Resolver } func (r *clipCommentResolver) Author(ctx context.Context, obj *domain.ClipComment) (*domain.User, error) { if obj.AuthorID == 0 { return nil, fmt.Errorf("comment has no author ID") } author, err := r.Services.User.GetByID(ctx, obj.AuthorID) if err != nil { return nil, fmt.Errorf("failed to get author: %w", err) } return author, nil }