aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/graphql/resolver
diff options
context:
space:
mode:
Diffstat (limited to 'backend/graphql/resolver')
-rw-r--r--backend/graphql/resolver/auth_helpers.go29
-rw-r--r--backend/graphql/resolver/pagination.go177
-rw-r--r--backend/graphql/resolver/resolver.go18
-rw-r--r--backend/graphql/resolver/schema.resolvers.go461
4 files changed, 0 insertions, 685 deletions
diff --git a/backend/graphql/resolver/auth_helpers.go b/backend/graphql/resolver/auth_helpers.go
deleted file mode 100644
index dcc09fb..0000000
--- a/backend/graphql/resolver/auth_helpers.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package resolver
-
-import (
- "context"
- "errors"
- "fmt"
-
- "github.com/labstack/echo/v4"
- appcontext "undef.ninja/x/feedaka/context"
-)
-
-// getUserIDFromContext retrieves the authenticated user ID from context
-// This is a wrapper around the GetUserID function from the context package
-func getUserIDFromContext(ctx context.Context) (int64, error) {
- userID, ok := appcontext.GetUserID(ctx)
- if !ok {
- return 0, fmt.Errorf("authentication required")
- }
- return userID, nil
-}
-
-// Helper function to get Echo context from GraphQL context
-func getEchoContext(ctx context.Context) (echo.Context, error) {
- echoCtx, ok := ctx.Value("echo").(echo.Context)
- if !ok {
- return nil, errors.New("echo context not found")
- }
- return echoCtx, nil
-}
diff --git a/backend/graphql/resolver/pagination.go b/backend/graphql/resolver/pagination.go
deleted file mode 100644
index 1a14650..0000000
--- a/backend/graphql/resolver/pagination.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package resolver
-
-import (
- "context"
- "fmt"
- "strconv"
-
- "undef.ninja/x/feedaka/db"
- "undef.ninja/x/feedaka/graphql/model"
-)
-
-const defaultPageSize = 30
-const maxPageSize = 100
-
-// articleRow is a common interface for all paginated article query rows.
-type articleRow struct {
- ID int64
- FeedID int64
- Guid string
- Title string
- Url string
- IsRead int64
- FeedID2 int64
- FeedUrl string
- FeedTitle string
- FeedIsSubscribed int64
-}
-
-func toArticleRow(r any) articleRow {
- switch v := r.(type) {
- case db.GetArticlesPaginatedRow:
- return articleRow{v.ID, v.FeedID, v.Guid, v.Title, v.Url, v.IsRead, v.FeedID2, v.FeedUrl, v.FeedTitle, v.FeedIsSubscribed}
- case db.GetArticlesPaginatedAfterRow:
- return articleRow{v.ID, v.FeedID, v.Guid, v.Title, v.Url, v.IsRead, v.FeedID2, v.FeedUrl, v.FeedTitle, v.FeedIsSubscribed}
- case db.GetArticlesByFeedPaginatedRow:
- return articleRow{v.ID, v.FeedID, v.Guid, v.Title, v.Url, v.IsRead, v.FeedID2, v.FeedUrl, v.FeedTitle, v.FeedIsSubscribed}
- case db.GetArticlesByFeedPaginatedAfterRow:
- return articleRow{v.ID, v.FeedID, v.Guid, v.Title, v.Url, v.IsRead, v.FeedID2, v.FeedUrl, v.FeedTitle, v.FeedIsSubscribed}
- default:
- panic("unexpected row type")
- }
-}
-
-func rowToArticle(row articleRow) *model.Article {
- return &model.Article{
- ID: strconv.FormatInt(row.ID, 10),
- FeedID: strconv.FormatInt(row.FeedID, 10),
- GUID: row.Guid,
- Title: row.Title,
- URL: row.Url,
- IsRead: row.IsRead == 1,
- Feed: &model.Feed{
- ID: strconv.FormatInt(row.FeedID2, 10),
- URL: row.FeedUrl,
- Title: row.FeedTitle,
- IsSubscribed: row.FeedIsSubscribed == 1,
- },
- }
-}
-
-func (r *queryResolver) paginatedArticles(ctx context.Context, isRead int64, feedID *string, after *string, first *int32) (*model.ArticleConnection, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- limit := int64(defaultPageSize)
- if first != nil {
- limit = int64(*first)
- if limit <= 0 {
- limit = int64(defaultPageSize)
- }
- if limit > maxPageSize {
- limit = maxPageSize
- }
- }
-
- // Fetch limit+1 to determine hasNextPage
- fetchLimit := limit + 1
-
- var rawRows []any
-
- if feedID != nil {
- parsedFeedID, err := strconv.ParseInt(*feedID, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid feed ID: %w", err)
- }
-
- if after != nil {
- cursor, err := strconv.ParseInt(*after, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid cursor: %w", err)
- }
- rows, err := r.Queries.GetArticlesByFeedPaginatedAfter(ctx, db.GetArticlesByFeedPaginatedAfterParams{
- IsRead: isRead,
- UserID: userID,
- FeedID: parsedFeedID,
- ID: cursor,
- Limit: fetchLimit,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to query articles: %w", err)
- }
- for _, row := range rows {
- rawRows = append(rawRows, row)
- }
- } else {
- rows, err := r.Queries.GetArticlesByFeedPaginated(ctx, db.GetArticlesByFeedPaginatedParams{
- IsRead: isRead,
- UserID: userID,
- FeedID: parsedFeedID,
- Limit: fetchLimit,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to query articles: %w", err)
- }
- for _, row := range rows {
- rawRows = append(rawRows, row)
- }
- }
- } else {
- if after != nil {
- cursor, err := strconv.ParseInt(*after, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid cursor: %w", err)
- }
- rows, err := r.Queries.GetArticlesPaginatedAfter(ctx, db.GetArticlesPaginatedAfterParams{
- IsRead: isRead,
- UserID: userID,
- ID: cursor,
- Limit: fetchLimit,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to query articles: %w", err)
- }
- for _, row := range rows {
- rawRows = append(rawRows, row)
- }
- } else {
- rows, err := r.Queries.GetArticlesPaginated(ctx, db.GetArticlesPaginatedParams{
- IsRead: isRead,
- UserID: userID,
- Limit: fetchLimit,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to query articles: %w", err)
- }
- for _, row := range rows {
- rawRows = append(rawRows, row)
- }
- }
- }
-
- hasNextPage := int64(len(rawRows)) > limit
- if hasNextPage {
- rawRows = rawRows[:limit]
- }
-
- articles := make([]*model.Article, 0, len(rawRows))
- for _, raw := range rawRows {
- articles = append(articles, rowToArticle(toArticleRow(raw)))
- }
-
- var endCursor *string
- if len(articles) > 0 {
- lastID := articles[len(articles)-1].ID
- endCursor = &lastID
- }
-
- return &model.ArticleConnection{
- Articles: articles,
- PageInfo: &model.PageInfo{
- HasNextPage: hasNextPage,
- EndCursor: endCursor,
- },
- }, nil
-}
diff --git a/backend/graphql/resolver/resolver.go b/backend/graphql/resolver/resolver.go
deleted file mode 100644
index dea85a0..0000000
--- a/backend/graphql/resolver/resolver.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package resolver
-
-import (
- "database/sql"
-
- "undef.ninja/x/feedaka/auth"
- "undef.ninja/x/feedaka/db"
-)
-
-// This file will not be regenerated automatically.
-//
-// It serves as dependency injection for your app, add any dependencies you require here.
-
-type Resolver struct {
- DB *sql.DB
- Queries *db.Queries
- SessionConfig *auth.SessionConfig
-}
diff --git a/backend/graphql/resolver/schema.resolvers.go b/backend/graphql/resolver/schema.resolvers.go
deleted file mode 100644
index 0392945..0000000
--- a/backend/graphql/resolver/schema.resolvers.go
+++ /dev/null
@@ -1,461 +0,0 @@
-package resolver
-
-// 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.76
-
-import (
- "context"
- "database/sql"
- "fmt"
- "strconv"
- "time"
-
- "undef.ninja/x/feedaka/auth"
- "undef.ninja/x/feedaka/db"
- "undef.ninja/x/feedaka/feed"
- gql "undef.ninja/x/feedaka/graphql"
- "undef.ninja/x/feedaka/graphql/model"
-)
-
-// AddFeed is the resolver for the addFeed field.
-func (r *mutationResolver) AddFeed(ctx context.Context, url string) (*model.Feed, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- // Fetch the feed to get its title
- f, err := feed.Fetch(ctx, url)
- if err != nil {
- return nil, fmt.Errorf("failed to parse feed: %w", err)
- }
-
- // Insert the feed into the database
- dbFeed, err := r.Queries.CreateFeed(ctx, db.CreateFeedParams{
- Url: url,
- Title: f.Title,
- FetchedAt: time.Now().UTC().Format(time.RFC3339),
- UserID: userID,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to insert feed: %w", err)
- }
-
- // Sync articles from the feed
- if err := feed.Sync(ctx, r.Queries, dbFeed.ID, f); err != nil {
- return nil, fmt.Errorf("failed to sync articles: %w", err)
- }
-
- return &model.Feed{
- ID: strconv.FormatInt(dbFeed.ID, 10),
- URL: dbFeed.Url,
- Title: dbFeed.Title,
- FetchedAt: dbFeed.FetchedAt,
- IsSubscribed: dbFeed.IsSubscribed == 1,
- }, nil
-}
-
-// UnsubscribeFeed is the resolver for the unsubscribeFeed field.
-func (r *mutationResolver) UnsubscribeFeed(ctx context.Context, id string) (bool, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return false, err
- }
-
- feedID, err := strconv.ParseInt(id, 10, 64)
- if err != nil {
- return false, fmt.Errorf("invalid feed ID: %w", err)
- }
-
- // Fetch feed
- feed, err := r.Queries.GetFeed(ctx, feedID)
- if err != nil {
- if err == sql.ErrNoRows {
- return false, fmt.Errorf("feed not found")
- }
- return false, fmt.Errorf("failed to query feed: %w", err)
- }
-
- // Check authorization
- if feed.UserID != userID {
- return false, fmt.Errorf("forbidden: you don't have access to this feed")
- }
-
- err = r.Queries.UnsubscribeFeed(ctx, feed.ID)
- if err != nil {
- return false, fmt.Errorf("failed to unsubscribe from feed: %w", err)
- }
-
- return true, nil
-}
-
-// MarkArticleRead is the resolver for the markArticleRead field.
-func (r *mutationResolver) MarkArticleRead(ctx context.Context, id string) (*model.Article, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- articleID, err := strconv.ParseInt(id, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid article ID: %w", err)
- }
-
- // Fetch article
- article, err := r.Queries.GetArticle(ctx, articleID)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("article not found")
- }
- return nil, fmt.Errorf("failed to query article: %w", err)
- }
-
- // Check authorization (article belongs to a feed owned by user)
- feed, err := r.Queries.GetFeed(ctx, article.FeedID)
- if err != nil {
- return nil, fmt.Errorf("failed to query feed: %w", err)
- }
- if feed.UserID != userID {
- return nil, fmt.Errorf("forbidden: you don't have access to this article")
- }
-
- // Update the article's read status
- err = r.Queries.UpdateArticleReadStatus(ctx, db.UpdateArticleReadStatusParams{
- IsRead: 1,
- ID: article.ID,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to mark article as read: %w", err)
- }
-
- // Fetch the updated article
- return r.Query().Article(ctx, id)
-}
-
-// MarkArticleUnread is the resolver for the markArticleUnread field.
-func (r *mutationResolver) MarkArticleUnread(ctx context.Context, id string) (*model.Article, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- articleID, err := strconv.ParseInt(id, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid article ID: %w", err)
- }
-
- // Fetch article
- article, err := r.Queries.GetArticle(ctx, articleID)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("article not found")
- }
- return nil, fmt.Errorf("failed to query article: %w", err)
- }
-
- // Check authorization (article belongs to a feed owned by user)
- feed, err := r.Queries.GetFeed(ctx, article.FeedID)
- if err != nil {
- return nil, fmt.Errorf("failed to query feed: %w", err)
- }
- if feed.UserID != userID {
- return nil, fmt.Errorf("forbidden: you don't have access to this article")
- }
-
- // Update the article's read status
- err = r.Queries.UpdateArticleReadStatus(ctx, db.UpdateArticleReadStatusParams{
- IsRead: 0,
- ID: article.ID,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to mark article as unread: %w", err)
- }
-
- // Fetch the updated article
- return r.Query().Article(ctx, id)
-}
-
-// MarkFeedRead is the resolver for the markFeedRead field.
-func (r *mutationResolver) MarkFeedRead(ctx context.Context, id string) (*model.Feed, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- feedID, err := strconv.ParseInt(id, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid feed ID: %w", err)
- }
-
- // Fetch feed
- feed, err := r.Queries.GetFeed(ctx, feedID)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("feed not found")
- }
- return nil, fmt.Errorf("failed to query feed: %w", err)
- }
-
- // Check authorization
- if feed.UserID != userID {
- return nil, fmt.Errorf("forbidden: you don't have access to this feed")
- }
-
- // Update all articles in the feed to be read
- err = r.Queries.MarkFeedArticlesRead(ctx, feed.ID)
- if err != nil {
- return nil, fmt.Errorf("failed to mark feed as read: %w", err)
- }
-
- // Fetch the updated feed
- return r.Query().Feed(ctx, id)
-}
-
-// MarkFeedUnread is the resolver for the markFeedUnread field.
-func (r *mutationResolver) MarkFeedUnread(ctx context.Context, id string) (*model.Feed, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- feedID, err := strconv.ParseInt(id, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid feed ID: %w", err)
- }
-
- // Fetch feed
- feed, err := r.Queries.GetFeed(ctx, feedID)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("feed not found")
- }
- return nil, fmt.Errorf("failed to query feed: %w", err)
- }
-
- // Check authorization
- if feed.UserID != userID {
- return nil, fmt.Errorf("forbidden: you don't have access to this feed")
- }
-
- // Update all articles in the feed to be unread
- err = r.Queries.MarkFeedArticlesUnread(ctx, feed.ID)
- if err != nil {
- return nil, fmt.Errorf("failed to mark feed as unread: %w", err)
- }
-
- // Fetch the updated feed
- return r.Query().Feed(ctx, id)
-}
-
-// Login is the resolver for the login field.
-func (r *mutationResolver) Login(ctx context.Context, username string, password string) (*model.AuthPayload, error) {
- // Verify user credentials
- user, err := r.Queries.GetUserByUsername(ctx, username)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("invalid credentials")
- }
- return nil, fmt.Errorf("failed to query user: %w", err)
- }
-
- // Verify password
- if !auth.VerifyPassword(user.PasswordHash, password) {
- return nil, fmt.Errorf("invalid credentials")
- }
-
- // Get Echo context to create session
- echoCtx, err := getEchoContext(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to get echo context: %w", err)
- }
-
- // Create session and store user ID
- if err := r.SessionConfig.SetUserID(echoCtx, user.ID); err != nil {
- return nil, fmt.Errorf("failed to create session: %w", err)
- }
-
- return &model.AuthPayload{
- User: &model.User{
- ID: strconv.FormatInt(user.ID, 10),
- Username: user.Username,
- },
- }, nil
-}
-
-// Logout is the resolver for the logout field.
-func (r *mutationResolver) Logout(ctx context.Context) (bool, error) {
- // Get Echo context to destroy session
- echoCtx, err := getEchoContext(ctx)
- if err != nil {
- return false, fmt.Errorf("failed to get echo context: %w", err)
- }
-
- // Destroy session
- if err := r.SessionConfig.DestroySession(echoCtx); err != nil {
- return false, fmt.Errorf("failed to destroy session: %w", err)
- }
-
- return true, nil
-}
-
-// Feeds is the resolver for the feeds field.
-func (r *queryResolver) Feeds(ctx context.Context) ([]*model.Feed, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- dbFeeds, err := r.Queries.GetFeeds(ctx, userID)
- if err != nil {
- return nil, fmt.Errorf("failed to query feeds: %w", err)
- }
-
- // Fetch unread counts for all feeds
- unreadCounts, err := r.Queries.GetFeedUnreadCounts(ctx, userID)
- if err != nil {
- return nil, fmt.Errorf("failed to query unread counts: %w", err)
- }
- countMap := make(map[int64]int64, len(unreadCounts))
- for _, uc := range unreadCounts {
- countMap[uc.FeedID] = uc.UnreadCount
- }
-
- var feeds []*model.Feed
- for _, dbFeed := range dbFeeds {
- feeds = append(feeds, &model.Feed{
- ID: strconv.FormatInt(dbFeed.ID, 10),
- URL: dbFeed.Url,
- Title: dbFeed.Title,
- FetchedAt: dbFeed.FetchedAt,
- IsSubscribed: dbFeed.IsSubscribed == 1,
- UnreadCount: int32(countMap[dbFeed.ID]),
- })
- }
-
- return feeds, nil
-}
-
-// UnreadArticles is the resolver for the unreadArticles field.
-func (r *queryResolver) UnreadArticles(ctx context.Context, feedID *string, after *string, first *int32) (*model.ArticleConnection, error) {
- return r.paginatedArticles(ctx, 0, feedID, after, first)
-}
-
-// ReadArticles is the resolver for the readArticles field.
-func (r *queryResolver) ReadArticles(ctx context.Context, feedID *string, after *string, first *int32) (*model.ArticleConnection, error) {
- return r.paginatedArticles(ctx, 1, feedID, after, first)
-}
-
-// Feed is the resolver for the feed field.
-func (r *queryResolver) Feed(ctx context.Context, id string) (*model.Feed, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- feedID, err := strconv.ParseInt(id, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid feed ID: %w", err)
- }
-
- // Fetch feed
- dbFeed, err := r.Queries.GetFeed(ctx, feedID)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("feed not found")
- }
- return nil, fmt.Errorf("failed to query feed: %w", err)
- }
-
- // Check authorization
- if dbFeed.UserID != userID {
- return nil, fmt.Errorf("forbidden: you don't have access to this feed")
- }
-
- return &model.Feed{
- ID: strconv.FormatInt(dbFeed.ID, 10),
- URL: dbFeed.Url,
- Title: dbFeed.Title,
- FetchedAt: dbFeed.FetchedAt,
- IsSubscribed: dbFeed.IsSubscribed == 1,
- }, nil
-}
-
-// Article is the resolver for the article field.
-func (r *queryResolver) Article(ctx context.Context, id string) (*model.Article, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- return nil, err
- }
-
- articleID, err := strconv.ParseInt(id, 10, 64)
- if err != nil {
- return nil, fmt.Errorf("invalid article ID: %w", err)
- }
-
- // Fetch article
- row, err := r.Queries.GetArticle(ctx, articleID)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("article not found")
- }
- return nil, fmt.Errorf("failed to query article: %w", err)
- }
-
- // Check authorization (article's feed belongs to user)
- // Note: GetArticle already joins with feeds table and returns feed info,
- // but we need to check the user_id. Since GetArticleRow doesn't include user_id,
- // we need to fetch the feed separately.
- feed, err := r.Queries.GetFeed(ctx, row.FeedID)
- if err != nil {
- return nil, fmt.Errorf("failed to query feed: %w", err)
- }
- if feed.UserID != userID {
- return nil, fmt.Errorf("forbidden: you don't have access to this article")
- }
-
- return &model.Article{
- ID: strconv.FormatInt(row.ID, 10),
- FeedID: strconv.FormatInt(row.FeedID, 10),
- GUID: row.Guid,
- Title: row.Title,
- URL: row.Url,
- IsRead: row.IsRead == 1,
- Feed: &model.Feed{
- ID: strconv.FormatInt(row.FeedID2, 10),
- URL: row.FeedUrl,
- Title: row.FeedTitle,
- },
- }, nil
-}
-
-// CurrentUser is the resolver for the currentUser field.
-func (r *queryResolver) CurrentUser(ctx context.Context) (*model.User, error) {
- userID, err := getUserIDFromContext(ctx)
- if err != nil {
- // Not authenticated - return nil (not an error)
- return nil, nil
- }
-
- user, err := r.Queries.GetUserByID(ctx, userID)
- if err != nil {
- if err == sql.ErrNoRows {
- return nil, nil
- }
- return nil, fmt.Errorf("failed to query user: %w", err)
- }
-
- return &model.User{
- ID: strconv.FormatInt(user.ID, 10),
- Username: user.Username,
- }, nil
-}
-
-// Mutation returns gql.MutationResolver implementation.
-func (r *Resolver) Mutation() gql.MutationResolver { return &mutationResolver{r} }
-
-// Query returns gql.QueryResolver implementation.
-func (r *Resolver) Query() gql.QueryResolver { return &queryResolver{r} }
-
-type mutationResolver struct{ *Resolver }
-type queryResolver struct{ *Resolver }