MTM-3: view reading list (most-recent-bookmarked first)#184
Conversation
Add current-user reading-list read paths over the MTM-6 bookmark primitive, newest-bookmarked-first with bookmark-time cursoring (D1): - ArticleQueryService.findUserBookmarks (offset/limit) and findUserBookmarksWithCursor (relay cursor); cursor anchored on article_bookmarks.created_at. - REST GET /articles/bookmarked (auth required) + WebSecurityConfig matcher. - GraphQL Query.bookmarkedArticles datafetcher + SDL. - New read-service methods (offset/limit ids, count, bookmark dates) + ArticleBookmarkDate / BookmarkedArticleData read models.
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| public ArticleDataList findUserBookmarks(User currentUser, Page page) { | ||
| List<String> ids = | ||
| articleBookmarksReadService.findUserBookmarkedArticleIds(currentUser.getId(), page); | ||
| int count = articleBookmarksReadService.countUserBookmarks(currentUser.getId()); | ||
| if (ids.size() == 0) { | ||
| return new ArticleDataList(new ArrayList<>(), count); | ||
| } | ||
| List<ArticleData> articles = findArticlesInIdOrder(ids); | ||
| fillExtraInfo(articles, currentUser); | ||
| return new ArticleDataList(articles, count); | ||
| } |
There was a problem hiding this comment.
🚩 Bookmark count includes orphaned bookmarks, creating a mismatch with returned articles
In findUserBookmarks at src/main/java/io/spring/application/ArticleQueryService.java:137, the total count comes from countUserBookmarks which counts raw bookmark rows, while findArticlesInIdOrder at line 141 filters out bookmarks whose articles no longer exist. This means articlesCount in the response can be higher than the number of articles actually returned. The test should_drop_bookmarks_for_deleted_articles at src/test/java/io/spring/application/article/ArticleBookmarkQueryServiceTest.java:237-240 explicitly documents and asserts this behavior (count=2 but only 1 article returned), so it appears intentional. However, clients paginating based on articlesCount may observe gaps or confusion, especially if many bookmarked articles are deleted.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This is intentional, as the test documents. articlesCount is the count of the user's bookmark rows (countUserBookmarks), independent of the page slice — matching the REST envelope semantics used elsewhere (articlesCount = total matching, not the page size). Orphaned bookmarks only arise because article_bookmarks has no FK to articles (MTM-6 foundation); in normal operation a deleted article wouldn't leave a dangling bookmark, so the count and result set agree. Reconciling orphan cleanup / FK semantics is a foundation concern outside MTM-3's additive scope, so I've left the count as the true bookmark total here rather than silently under-reporting.
When every article on a bookmark page has been deleted, findArticlesInIdOrder returns empty while bookmark ids are non-empty; calling fillExtraInfo then renders an invalid 'where id in ()' query. Skip fillExtraInfo for the empty case in both the offset/limit and cursor paths; add a regression test.
…82442947-mtm-3-reading-list
…MTM-4) - Merge updated base so this branch builds against MTM-4's bookmarked flag. - Skip articles whose bookmark was concurrently removed (null bookmark timestamp) to avoid an NPE when building the cursor.
Summary
Implements MTM-3 — a signed-in user can view their reading list, newest‑bookmarked first — on top of the MTM-6 bookmark persistence foundation. All additive: a query‑service method, a REST path, a GraphQL
Queryfield, aWebSecurityConfigmatcher, and two read models. No changes to the per‑articlebookmarkedfield (owned by MTM-4, per D5).Ordering and cursoring are anchored on
article_bookmarks.created_at(D1), independent of the article's own create/update time.Subtasks
ArticleQueryServicefindUserBookmarks(User, Page)→ offset/limit, returnsArticleDataList(REST).findUserBookmarksWithCursor(User, CursorPageParameter<DateTime>)→ relay cursor (GraphQL), mirrorsfindRecentArticlesWithCursor(hasExtra trim + reverse‑on‑PREV).GET /articles/bookmarked(auth required),offsetdefault 0 /limitdefault 20, envelope{ "articles": [...], "articlesCount": <int> }; explicitauthenticated()matcher inWebSecurityConfig(sinceGET /articles/**is otherwisepermitAll).bookmarkedArticles(first, after, last, before): ArticlesConnection; datafetcher builds the connection exactly likeuserFavorites(edges/cursors/pageInfo + slug→ArticleDatalocalContext);getCurrentUser().orElseThrow(AuthenticationException::new).Key implementation note — cursor vs. storage format
articles.created_atis persisted as INTEGER epoch‑millis (written via the jodaDateTimeHandler), butarticle_bookmarks.created_atis persisted as TEXT (the column'sCURRENT_TIMESTAMPdefault from MTM-6). Feeding the MTM-6 cursor primitive aDateTimecursor binds it as millis, which can't compare correctly against the TEXT column under SQLite affinity. To keep the client‑facing cursor consistent (DateTimeCursor= millis) while comparing correctly, the cursor is converted to SQLite text form only at the read boundary — MTM-6's mapper and write path are untouched:BookmarkedArticleData implements Nodecarries the bookmarkDateTimesogetCursor()returnsDateTimeCursor(bookmarkedAt).Robustness (from review)
fillExtraInfowhen the page's articles are all deleted — otherwise the favorites/bookmarks mappers renderwhere id in ()(invalid SQL → 500). Applies to both offset and cursor paths; covered by a new test.DateTimeCursorand correctly drops the entry.Files
application/data/BookmarkedArticleData.java,application/data/ArticleBookmarkDate.java.application/ArticleQueryService.java,api/ArticlesApi.java,api/security/WebSecurityConfig.java,graphql/ArticleDatafetcher.java(+ widenedbuildArticlePageInfotoCursorPager<? extends Node>),mybatis/readservice/ArticleBookmarksReadService.java(+3 methods),mapper/ArticleBookmarksReadService.xml,schema/schema.graphqls.ArticleBookmarkQueryServiceTest,BookmarkedArticlesApiTest,ArticleBookmarkDatafetcherTest, additions toArticleBookmarksReadServiceTest.feat/reading-list(MTM-2 / MTM-4) so this branch builds against the sharedbookmarkedflag.Tests (MTM-18)
created_at; REST{articles, articlesCount}; GraphQL edges/cursors/pageInfo + localContext.first/after+last/beforestable, non‑overlapping;hasNextPage/hasPreviousPagecorrect.AuthenticationException(GraphQL).bookmarkedflag.Verification gate
JaCoCo on new code = 98.8% (340/344 instructions; every new method 100%). CI
build+ Devin Review checks are green.Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/d1d174cc645441b9b27945249d17f070