Skip to content

feat(profile): expose published articleCount on the profile response - #214

Merged
aquie00t merged 1 commit into
mainfrom
feat/profile-article-count
Aug 25, 2026
Merged

feat(profile): expose published articleCount on the profile response#214
aquie00t merged 1 commit into
mainfrom
feat/profile-article-count

Conversation

@aquie00t

Copy link
Copy Markdown
Collaborator

What does this PR do?

The profile carried followersCount, followingCount and postCount but said nothing about articles, even though the article feature has been merged for several releases. GET /api/v1/profiles/:username now also returns articleCount.

Published-only, and the same for everyone

The count excludes drafts and archived articles — including when the viewer is the owner. Two reasons:

  • A total that included drafts would leak both the existence and the volume of unpublished work, which is exactly what the published-only rule on every public path exists to protect.
  • A viewer-dependent number would be unstable, and it would fork a code path that is otherwise identical for owner, stranger and guest.

An author's own draft count is available as the meta.total of GET /articles/me?status=DRAFT.

E2E pins all of this: a draft does not move the number, publishing does, archiving moves it back down, and owner / stranger / guest all see the same value.

The query

countPublishedByAuthorId is a single prisma.article.count({ where: { authorId, status: PUBLISHED } }), covered exactly by the existing @@index([authorId, status]). It joins the Promise.all that already fetches the follow status and the post count, so the profile still makes one round of parallel queries.

Deliberately not findAll({ authorId, limit: 1 }) reading total — that would run a findMany with the full relation include just to produce a number.

Note for review: this class of change is not type-checked

Adding a required property to ProfileItemSchema without populating it in the controller makes fast-json-stringify fail serialization → 500. TypeScript does not catch it: controller methods take a bare FastifyReply, so send() is never compared against the route's response generic.

The e2e test is the only thing between that and a production 500, which is why this PR adds four of them.

Verification

lint, format:check, build clean; 759 unit tests pass (755 + 4 new).

  • UnitarticleCount comes from the repository, is called with profile.userId, is identical for owner/stranger/guest, is reported alongside postCount rather than instead of it, and is not queried at all when the profile does not exist.
  • IntegrationcountPublishedByAuthorId counts published only, ignores drafts, drops back when an article is archived, ignores another author's articles, and returns 0 for an unknown id.
  • E2E — the four behaviours above against real HTTP.

No DI, Cradle, route or app.ts change: GetProfileUseCase is asClass(...).singleton() and awilix CLASSIC resolves the new articleRepository parameter by name from the key already registered in persistence.di.ts.

Client contract for the profile Articles tab

No new endpoint. The existing filter already does this job, with cache invalidation wired into publish/archive/update/delete:

  • Public tab → GET /api/v1/articles?authorUsername=<username>&page=&limit=
  • Own drafts → GET /api/v1/articles/me?status=DRAFT

Second of three. Stacks on #213, which slims the list payload those tabs will be reading.


Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Chore

Checklist

  • My branch follows the naming convention (feature/, fix/, chore/, docs/)
  • My commits follow Conventional Commits
  • I have tested my changes locally
  • I have not introduced any breaking changes
  • I have updated relevant documentation if needed

🤖 Generated with Claude Code

The profile carried followersCount, followingCount and postCount but said
nothing about articles, even though the article feature has been merged for
several releases.

The count is published-only, and identical for every viewer including the
owner. A total that included drafts would leak both the existence and the
volume of unpublished work, and a viewer-dependent number would be unstable
and would fork a code path that is otherwise the same for everyone. An
author's own draft count is the meta of GET /articles/me?status=DRAFT.

countPublishedByAuthorId is a single count query covered exactly by the
existing (author_id, status) index, rather than reusing findAll with limit 1
and reading total - that would run a findMany with the full relation include
to produce a number.

It joins the Promise.all that already fetches the follow status and the post
count, so the profile still makes one round of parallel queries.

Note for review: adding a required property to ProfileItemSchema is not
type-checked anywhere. Controller methods take a bare FastifyReply, so send()
is never compared against the route's response generic, and fast-json-stringify
fails serialization on a missing required property. The e2e test is the only
thing standing between that and a 500, which is why this change adds four.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aquie00t
aquie00t merged commit 1b030bf into main Aug 25, 2026
10 checks passed
github-actions Bot pushed a commit that referenced this pull request Aug 25, 2026
# [1.7.0](v1.6.0...v1.7.0) (2026-08-25)

### Features

* **article:** drop the markdown body from list responses ([#213](#213)) ([628d897](628d897))
* **profile:** expose published articleCount on the profile response ([#214](#214)) ([1b030bf](1b030bf))

### Performance Improvements

* **profile:** resolve follower list targets without loading the full profile ([#215](#215)) ([b855269](b855269))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.7.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant