feat(api): add GET /content/posts list endpoint#184
Conversation
Adds a paginated post-card endpoint (playfulprogramming#83) with newest/oldest sort, co-author filtering, and tags aggregated from post_tags. Extracts a shared PostBaseSchema so playfulprogramming#80's post-detail schema and this list-item schema compose from common fields instead of duplicating them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 56 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR adds ChangesPosts List Endpoint
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant postsRoutes
participant Drizzle
participant Database
Client->>postsRoutes: GET /content/posts with query parameters
postsRoutes->>Drizzle: Query filtered and paginated post rows
Drizzle->>Database: Join postData and postTags
Database-->>Drizzle: Post rows with aggregated tags
postsRoutes->>Drizzle: Query authors for returned post slugs
Drizzle->>Database: Fetch author and profile fields
Database-->>Drizzle: Author rows
postsRoutes->>postsRoutes: Group authors and map response cards
postsRoutes-->>Client: 200 PostsResponse
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/api/src/routes/content/posts.ts (1)
101-102: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider conditional
postAuthorsjoin when no author filter is applied.The
leftJoin(postAuthors)is always applied, even when noauthorfilter is present. This produces N rows per post (one per author), creating a cross product withpostTagsrows thatgroupBymust collapse. For a post with 5 tags and 3 authors, that's 15 rows instead of 5. Using a subquery orEXISTSfor the author filter, or conditionally applying the join, would avoid the unnecessary cross product.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/routes/content/posts.ts` around lines 101 - 102, The `getPosts` query in `posts.ts` always applies the `leftJoin(postAuthors)` even when no author filter is provided, which creates an unnecessary tag/author row explosion before `groupBy` collapses it. Update the query builder so `postAuthors` is only joined when the `author` filter is actually used, or replace that join with an `EXISTS`/subquery-based author filter; keep the existing `postTags` and `groupBy` behavior intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/routes/content/posts.ts`:
- Around line 17-18: The query schema for the posts route currently makes page
and limit required and leaves limit unbounded. Update the schema in the content
posts route so the request handler for GET /content/posts uses defaults of
page=1 and limit=20, and add a maximum constraint to limit to prevent oversized
requests. Make the change in the schema definitions used by the posts route so
the route behavior matches the pagination defaults expected by the handler.
- Around line 96-102: Select a single postData version per post/locale in the
posts query, since the current posts -> postData inner join on slug and locale
can duplicate rows when multiple published versions exist. Update the join logic
in the query that builds the posts list to constrain postData to the intended
version (for example by joining through a version-aware subquery or adding the
version filter used by the sync job) before the later grouping/sorting, and keep
the change localized to the route handler that assembles the posts query.
---
Nitpick comments:
In `@apps/api/src/routes/content/posts.ts`:
- Around line 101-102: The `getPosts` query in `posts.ts` always applies the
`leftJoin(postAuthors)` even when no author filter is provided, which creates an
unnecessary tag/author row explosion before `groupBy` collapses it. Update the
query builder so `postAuthors` is only joined when the `author` filter is
actually used, or replace that join with an `EXISTS`/subquery-based author
filter; keep the existing `postTags` and `groupBy` behavior intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b42b80f2-ff7f-4749-9add-9a44fc6c12b2
📒 Files selected for processing (6)
apps/api/src/createApp.tsapps/api/src/routes/content/post.tsapps/api/src/routes/content/postBaseSchema.tsapps/api/src/routes/content/posts.test.tsapps/api/src/routes/content/posts.tsapps/api/test-utils/setup.ts
…fulprogramming#184) Bound pagination (page defaults to 0, limit defaults to 20 with a max of 100), pin the postData join to version="" to stop duplicate rows when a slug/locale has multiple versions, and replace the always-on postAuthors join with a conditional EXISTS filter so it no longer fans out the postTags aggregation when no author filter is given. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bound pagination (page defaults to 0, limit defaults to 20 with a max of 100), pin the postData join to version="" to stop duplicate rows when a slug/locale has multiple versions, and replace the always-on postAuthors join with a conditional EXISTS filter so it no longer fans out the postTags aggregation when no author filter is given. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Closes #83. Adds a paginated
GET /content/postsendpoint returning post-card data: banner, title, authors, word count, publish date, and tags.page/limit, 0-indexed to match/content/profilesand/content/collections.pagedefaults to0,limitdefaults to20with a maximum of100.sort=newest(default, descending bypublishedAt) orsort=oldest(ascending).author=<slug>matches anywhere in a post's co-author list, not just a primary author, via anEXISTSsubquery againstpost_authors.post_tagsjoin table (added in Create a tags table and relations for posts/collections #81) via the core Drizzle query builder (leftJoin+groupBy+array_agg), mirroring the pattern from Add posts and sortBy=posts to /content/profiles #176's profiles post-count work.excerpt/descriptionis only used internally to backfill a missing description at sync time and is intentionally not part of this response.sync-post(postData.wordCount) — no migration or worker changes needed here, this endpoint just reads the existing column.Schema refactor
Extracted a shared
PostBaseSchema(slug,title,bannerUrl,wordCount,publishedAt,authors) so the #80 post-detail schema and this new list-item schema compose from common fields viaType.Intersectinstead of duplicating them. Note: TypeBox'sType.Compositeisn't actually available in the installedtypeboxversion (only exists in@sinclair/typebox0.34.x per the readme's legacy migration notes) —Type.Intersectis the real composition primitive here. Verified with a throwaway Fastify test that the compiledfast-json-stringifyserializer correctly flattens theIntersect/allOfschema (both branches' fields present, undeclared fields stripped) rather than silently falling back to plainJSON.stringify.Query design
Author objects for the response are fetched via a small second query (
postAuthors+profilesfor the page's post slugs) rather than folded into the main aggregate query, to avoid needingjson_aggof multi-field objects alongside the tagsarray_agg.Open design question:
postDataversion selectionpostDatais keyed by(slug, locale, version), andsync-postwrites one row per version, so a naive join on justslug+localecan return duplicate rows for a post that has more than one version. This PR pins the join toversion = ""to resolve that — matching the schema's declared default and what every existing fixture assumes today.Flagging rather than assuming this is settled: there's no documented "current version" convention anywhere in the query layer today. #80's
post.tshas the same latent gap — it doesn't filter byversionat all, it just takesdata[0]from an unordered, unfiltered result. If Playful Programming's post-rewrite/versioning feature ever populates a real non-emptyversionfor live content, this filter andpost.ts'sdata[0]pick will both need revisiting together, ideally by establishing one real convention (e.g. anisCurrentflag, or "max version wins") rather than each endpoint guessing independently. See linked issue once opened.Test plan
pnpm test:unitpasses (sherif, knip, eslint, publint, vitest across all projects)pnpm prettiercheck clean (aside from the pre-existing.claude/settings.local.jsonexclusion)posts.test.ts: pagination (including bounds/defaults), sort default/oldest, author co-author filtering viaEXISTS, tag aggregation, multi-author grouping, empty results, and confirms nodescription/excerptfield leaks into the responsepost.test.tsstill passes unchanged after the schema refactorSummary by CodeRabbit
New Features
Bug Fixes
Tests