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/pagination.go | 177 --------------------------------- 1 file changed, 177 deletions(-) delete mode 100644 backend/graphql/resolver/pagination.go (limited to 'backend/graphql/resolver/pagination.go') 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 -} -- cgit v1.3-1-g0d28