From 2889b562e64993482bd13fd806af8ed0865bab8b Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 14 Feb 2026 11:52:56 +0900 Subject: refactor: migrate API from GraphQL to REST (TypeSpec/OpenAPI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the entire GraphQL stack (gqlgen, urql, graphql-codegen) with a TypeSpec → OpenAPI 3.x pipeline using oapi-codegen for Go server stubs and openapi-fetch + openapi-typescript for the frontend client. Co-Authored-By: Claude Opus 4.6 --- backend/graphql/resolver/schema.resolvers.go | 461 --------------------------- 1 file changed, 461 deletions(-) delete mode 100644 backend/graphql/resolver/schema.resolvers.go (limited to 'backend/graphql/resolver/schema.resolvers.go') 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 } -- cgit v1.3-1-g0d28