Skip to content

feat(post): list a post's quotes and allow a quote with no text - #242

Merged
aquie00t merged 4 commits into
mainfrom
feature/post-quote-list
Aug 30, 2026
Merged

feat(post): list a post's quotes and allow a quote with no text#242
aquie00t merged 4 commits into
mainfrom
feature/post-quote-list

Conversation

@aquie00t

Copy link
Copy Markdown
Collaborator

Alıntı özelliğinin son PR'ı. Base'i main değil, #241'in dalı (feature/post-quote-notification) — zincir tek seferde merge edilip tek deploy olacak.

Ne değişti

  • GET /posts/:id/quotes — bir postu alıntılayanların sayfalanmış listesi, en yeniden eskiye. Her satır tam bir post: kendi quotedPost kartıyla ve giriş yapmış çağıran için isLiked / isBookmarked doldurulmuş halde.
  • Metinsiz alıntıcontent artık boş olabilir, ama yalnızca quotedPostId ile birlikte. Alıntısız boş post hâlâ 400.

Kararlar

  • Yeni repository metodu yok. findAll'a quotedPostId filtresi eklendi; sayfalama, include ve çağıranın like/bookmark kapsamı zaten orada yaşıyor, paralel bir sorgu zamanla kayardı.
  • Post önce çözülüyor. Olmayan bir post için 404; "alıntısı yok" ile "böyle bir post yok" istemci için farklı cevaplar, GET /posts/:id de böyle davranıyor.
  • Boş içerik kuralı use-case'te. İki alan arasında bir kural ve bir iş kuralı; TypeBox'ta anyOf ile ifade edilebilirdi ama ajv'nin ürettiği hata mesajı okunmaz olurdu. Mevcut "boş içerik 400" e2e testi yeşil kalıyor — hata artık şemadan değil use-case'ten geliyor.
  • Meta feed'inkiyle aynı (total, currentPage, limit, totalPages), yorum listesinin ince meta'sı değil — dönen şey bir post listesi.

Doğrulama

  • pnpm test:unit → 901/901 geçti. Yeni: get-post-quotes.usecase.test.ts (4 durum), create-post'a boş içerik için 2 durum.
  • pnpm lint, pnpm format:check, tsc --noEmit temiz. Migration yok.
  • Gerçek DB smoke, 8/8: yalnızca o postun alıntıları listeleniyor, en yeniden eskiye, her satır kartını taşıyor, sayfalama listede ilerliyor, ilgisiz post kendi alıntısını görüyor, olmayan post 404 (boş sayfa değil), metinsiz alıntı yazılabiliyor, alıntısız boş post reddediliyor.
  • Integration (+1) ve E2E (yeni get-quotes.test.ts, 6 durum + create'e 1) CI'da koşacak.

Zincir tamam

#239#240#241 → bu. Dördü birlikte merge edilecek.

🤖 Generated with Claude Code

https://claude.ai/code/session_015JC6UgjwRSJ3KHToPYqBPC

aquie00t and others added 4 commits August 30, 2026 09:26
A post can now quote another post, the way a quote tweet does: POST /posts
takes an optional quotedPostId and every post response carries a quotedPost
card, or null.

The relation is self-referential on Post and cascades, so deleting a post
deletes every post that quotes it and a card can never point at content that
is gone. The card is deliberately lean - author, content, media and date, no
counters and no viewer-specific flags - so embedding one costs no extra joins,
and the include stops at one level, so a quote of a quote carries the post it
quotes and nothing behind it.

The quoted post is resolved before the write, so quoting an id that is already
gone answers 404 rather than a constraint violation. The feed cache revives the
nested date by hand: without it the same request would answer with a Date on a
miss and a string for the next 60 seconds.

Counters, notifications and a list of quoters follow in their own changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JC6UgjwRSJ3KHToPYqBPC
Posts carry a quoteCount alongside likeCount and commentCount, so a client can
draw the quote badge without counting rows. Denormalised for the same reason
the other two are: the read path is far hotter than the write path, and
counting the quotes relation would put a subquery on every feed item.

Keeping that counter honest makes post creation a multi-step write, so
CreatePostUseCase moves onto TransactionPort: the post, the quoted post's
existence check and the increment now commit or roll back together. A post
that existed without having been counted would leave the badge permanently
short with nothing to notice it afterwards. The bot check stays outside the
transaction because it only reads, and so do the cache purge and the follower
fan-out, which must not hold the write open or roll it back.

Deleting a quote gives the count back in the same transaction as the delete.
Deleting a quoted post needs no such care: its quotes are cascaded away with
it and no surviving row was counting them.

Known drift, matching likeCount today: hard-deleting a user cascades their
posts away without decrementing the posts those quoted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JC6UgjwRSJ3KHToPYqBPC
Being quoted is a louder signal than a like - somebody has said something
about your post to their own followers - and until now it arrived silently,
as a number going up.

The notification leads to the quote rather than to the post being quoted: the
recipient already knows their own post, what they want to open is what was
said about it, and the quote carries the original as its card anyway. That
also buys the cleanup for free, since Notification.post cascades - deleting
the quote takes its notification with it, with no undo path to write.

NotifyQuotedAuthorUseCase sits beside NotifyNewPostUseCase and resolves the
quoted author itself, so post creation gains one collaborator rather than two
and its transaction keeps its shape. It runs after the commit and
fire-and-forget: the post is the thing worth keeping, so a notification
failure is logged rather than allowed to fail the request or roll the write
back. An account quoting itself notifies nobody.

First enum change in the repository; the migration carries the note about
ALTER TYPE ... ADD VALUE inside Prisma's migration transaction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JC6UgjwRSJ3KHToPYqBPC
Two gaps left by the earlier pieces of the feature.

A post says "12 quotes" and there was nowhere to go from there. GET
/posts/:id/quotes returns that page, newest first, each row a full post with
its quote card and, for a signed-in caller, their own like and bookmark state.
It reuses findAll rather than adding a parallel query: pagination, includes
and viewer scoping already live there, and a second copy would drift. The post
itself is resolved first, so "this post has no quotes" and "there is no such
post" stay different answers.

A quote with nothing added to it - a plain repost - is the most common shape
of the feature and the schema forbade it. Content may now be empty, but only
alongside quotedPostId; an empty post that quotes nothing is still rejected.
That rule spans two fields and reads as a business rule, so it lives in the
use case rather than as an unreadable anyOf in the request schema.

This completes the quote feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JC6UgjwRSJ3KHToPYqBPC
@aquie00t
aquie00t changed the base branch from feature/post-quote-notification to main August 30, 2026 14:03
@aquie00t
aquie00t merged commit faa97c7 into main Aug 30, 2026
@aquie00t
aquie00t deleted the feature/post-quote-list branch August 30, 2026 14:04
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.

1 participant