Skip to content

feat(api): add post count and sortBy to GET /content/profiles#183

Open
bbornino wants to merge 2 commits into
playfulprogramming:mainfrom
bbornino:176-profiles-post-count
Open

feat(api): add post count and sortBy to GET /content/profiles#183
bbornino wants to merge 2 commits into
playfulprogramming:mainfrom
bbornino:176-profiles-post-count

Conversation

@bbornino

@bbornino bbornino commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #176.

Extends GET /content/profiles with a posts count per author and a sortBy query param (id | posts), so the Search page can render a "top N authors by post count" list.

  • Switched the route from db.query.profiles.findMany (relational query API) to db.select (core query builder), since ordering by an aggregated to-many count while paginating correctly isn't supported by the relational API.
  • profiles → left join postAuthors → left join postData, filtered to publishedAt is not null and noindex = false in the join condition (same draft-post visibility convention as Endpoint for get post by ID #80's /content/post/:slug endpoint), grouped by profile, with countDistinct(postData.slug) as the post count. postData is keyed per locale/version, so countDistinct avoids double-counting a post published in multiple locales.
  • sortBy defaults to id (existing behavior: profiles.slug ascending, unchanged) or posts (post count descending, most active authors first).
  • Added posts as a required field on the response schema, with updated example payloads.

Note: the issue text described joining postAuthors to posts and filtering posts by publishedAt/noindex — but those columns actually live on postData, not posts, so the query joins postData directly (skipping an unnecessary extra hop through posts, since postAuthors.postSlug already FK-references posts.slug).

Test plan

  • pnpm test:unit (lint, knip, publint, sherif, vitest across all NX projects)
  • pnpm prettier (check mode)
  • Extended profiles.test.ts: default sortBy=id unchanged, sortBy=posts ordering, and posts without a publishedAt or with noindex: true excluded from the count (verified via the join's filter predicate, since the DB layer is mocked in this test suite)
  • Also updated the shared apps/api/test-utils/setup.ts @playfulprogramming/db mock to expose profiles/postAuthors/postData table placeholders and db.select, needed for the new query builder usage (existing tests for other routes unaffected — all 29 API tests pass)

Summary by CodeRabbit

  • New Features

    • Profiles can now be sorted using a query option, including sorting by post count.
    • Profile responses now include a post count for each profile.
  • Bug Fixes

    • Improved profile listing to better reflect published, indexable posts.
    • Profile image URLs continue to be returned consistently when available.
  • Tests

    • Expanded coverage for sorting, pagination, joins, and response shape changes.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@bbornino, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8a2e6500-612d-4860-92d6-ee3fd9fe86f1

📥 Commits

Reviewing files that changed from the base of the PR and between 749e870 and acee66e.

📒 Files selected for processing (2)
  • apps/api/src/routes/content/profiles.test.ts
  • apps/api/src/routes/content/profiles.ts
📝 Walkthrough

Walkthrough

The /content/profiles endpoint is reworked to compute a post count per profile via a joined select query and to support a new sortBy query parameter (id or posts). The response schema gains a posts field, and tests/mocks are updated to reflect the new Drizzle select-chain query pattern.

Changes

Profiles Endpoint Post Count and Sorting

Layer / File(s) Summary
Query params and response schema
apps/api/src/routes/content/profiles.ts
Adds a sortBy query parameter ("id" default or "posts") and a posts field to ProfilesResponseSchema, with updated example entries.
Joined select query and handler logic
apps/api/src/routes/content/profiles.ts
Replaces db.query.profiles.findMany with a joined/aggregated select counting distinct published, non-noindex posts per profile, orders results conditionally based on sortBy, and maps posts/profileImageUrl into responses.
Test setup mocks
apps/api/test-utils/setup.ts
Adds mocked profiles, postAuthors, postData schema objects and a db.query.select mock function to support the new query pattern.
Select-chain mock helper and profiles tests
apps/api/src/routes/content/profiles.test.ts
Introduces mockSelectChain to emulate the Drizzle query pipeline, updates snapshots/assertions for posts and profileImageUrl, and adds tests for default sorting, sortBy=posts ordering, pagination, and join filter conditions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ProfilesRoute
  participant DrizzleDB

  Client->>ProfilesRoute: GET /content/profiles?sortBy=posts&page=1&limit=10
  ProfilesRoute->>DrizzleDB: select profiles left-joined with postAuthors, postData
  DrizzleDB-->>ProfilesRoute: rows filtered by publishedAt not-null and noindex=false, grouped and counted
  ProfilesRoute->>ProfilesRoute: apply orderBy (slug asc or postsCount desc), limit/offset
  ProfilesRoute-->>Client: JSON profiles with posts count and profileImageUrl
Loading

Related issues: #176 (Add posts and sortBy=posts to /content/profiles)

Suggested reviewers: playfulprogramming maintainers familiar with the /content/profiles endpoint and Drizzle query patterns.

Poem:
A rabbit counts each post with care,
Joins and filters, tidy and fair,
Sort by id, or sort by fame,
posts now ride along the same,
Hop, hop—the profiles query's there! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding post counts and sortBy support to GET /content/profiles.
Linked Issues check ✅ Passed The PR implements the requested default id/posts sorting and returns per-author post counts for /content/profiles.
Out of Scope Changes check ✅ Passed The changes shown are limited to the endpoint, its tests, and supporting test setup, with no unrelated scope visible.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/src/routes/content/profiles.test.ts (1)

3-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing coverage: first join predicate (postAuthorsprofiles) is never asserted.

mockSelectChain exposes leftJoinPostAuthors, but no test asserts what it was called with. Only the second join (leftJoinPostData, publishedAt/noindex filter) is verified. A regression that swaps or breaks eq(postAuthors.authorSlug, profiles.slug) in profiles.ts would go undetected.

♻️ Suggested addition
 		test("only counts posts with a publishedAt date and noindex false", async () => {
 			const chain = mockSelectChain([]);
 
 			const response = await app.inject({
 				method: "GET",
 				url: "/content/profiles",
 				query: { page: "0", limit: "10" },
 			});
 
 			expect(response.statusCode).toBe(200);
+			expect(chain.leftJoinPostAuthors).toBeCalledWith(
+				postAuthors,
+				eq(postAuthors.authorSlug, profiles.slug),
+			);
 			expect(chain.leftJoinPostData).toBeCalledWith(
 				postData,
 				and(
 					eq(postData.slug, postAuthors.postSlug),
 					isNotNull(postData.publishedAt),
 					eq(postData.noindex, false),
 				),
 			);
 		});

Also applies to: 157-175

🤖 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/profiles.test.ts` around lines 3 - 28, Add test
coverage for the first join in mockSelectChain by asserting leftJoinPostAuthors
is called with the expected join predicate between postAuthors and profiles.
Update the profiles test cases that exercise the query builder in profiles.ts so
they verify eq(postAuthors.authorSlug, profiles.slug) is used, alongside the
existing assertions for leftJoinPostData and the publishedAt/noindex filter.
🤖 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/profiles.ts`:
- Around line 97-101: The sorting in the profiles query is unstable when sortBy
is "posts" because the orderBy call only uses countDistinct(postData.slug),
which can reshuffle equal-count profiles across pages. Update the ordering in
the profiles route query so the sortBy === "posts" branch uses
desc(countDistinct(postData.slug)) with asc(profiles.slug) as a tiebreaker,
keeping pagination deterministic while preserving the existing sort logic.

---

Nitpick comments:
In `@apps/api/src/routes/content/profiles.test.ts`:
- Around line 3-28: Add test coverage for the first join in mockSelectChain by
asserting leftJoinPostAuthors is called with the expected join predicate between
postAuthors and profiles. Update the profiles test cases that exercise the query
builder in profiles.ts so they verify eq(postAuthors.authorSlug, profiles.slug)
is used, alongside the existing assertions for leftJoinPostData and the
publishedAt/noindex filter.
🪄 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: f3248282-07fd-4130-9e76-60b76c63b7f2

📥 Commits

Reviewing files that changed from the base of the PR and between 086e182 and 749e870.

📒 Files selected for processing (3)
  • apps/api/src/routes/content/profiles.test.ts
  • apps/api/src/routes/content/profiles.ts
  • apps/api/test-utils/setup.ts

Comment thread apps/api/src/routes/content/profiles.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add posts and sortBy=posts to /content/profiles

1 participant