Skip to content

feat(notification): carry the whole deep-link target on every notification - #221

Merged
aquie00t merged 1 commit into
mainfrom
feat/notification-deep-links
Aug 26, 2026
Merged

feat(notification): carry the whole deep-link target on every notification#221
aquie00t merged 1 commit into
mainfrom
feat/notification-deep-links

Conversation

@aquie00t

Copy link
Copy Markdown
Collaborator

Closes findings 1-5 of the notification audit. Findings 6-7 (unread-count endpoint, per-notification read) follow in a second PR.

Problem

Tapping a notification could not reliably open what it was about.

  1. No referenceId on two types. like-post.usecase.ts and like-comment.usecase.ts called Notification.create without a fourth argument, so the stored row had referenceId = null. The realtime event carried the right ids, the persisted notification did not — a link that worked live and broke once the user opened the list.
  2. LIKE did not distinguish post from article. Both used the same type; the client could not tell what the uuid it received pointed at.
  3. Comment notifications stored only the comment id. Opening a comment means opening the post or article it lives under, which the row did not know.
  4. Articles are read by slug (GET /articles/:slug), but notifications only knew the article uuid, and no endpoint resolves one into the other.
  5. The notification id was never mapped, so a single notification could not be addressed — no per-item read, no stable list key.

Change

Notification now stores postId / articleId / commentId alongside referenceId, and Notification.create takes a NotificationTarget instead of a bare reference:

Notification.create(recipientId, issuerId, NotificationType.COMMENT_LIKE, {
    commentId: comment.id,
    postId: comment.postId ?? undefined,
    articleId: comment.articleId ?? undefined,
});

referenceId is derived from that target — the comment, else the article, else the post — so it is populated consistently and every old client keeps working unchanged.

  • Article slugs are resolved on read, not denormalised: the repository includes article: { select: { slug: true } }, so articleSlug is always current even after a title change.
  • The target columns are real foreign keys with ON DELETE CASCADE. A notification can no longer outlive the content it points at, so a tap never lands on a 404. This also covers part of finding 8.
  • The response exposes id, unblocking the per-notification read endpoint in the follow-up PR.
  • The realtime payload gained articleSlug, so the live event and the REST row build the same URL.

Client rule after this change: commentId → open the post/article and scroll to the comment; articleId/articles/{articleSlug}; postId → the post; none of them → the issuer's profile via username.

Migration

20260826233221_notification_deep_link_targets adds three nullable columns, their indexes and their cascading foreign keys. Existing rows keep their referenceId and get NULL targets — they render as before and simply are not deep-linkable.

Verification

  • pnpm test:unit — 785 passed
  • pnpm test:integration — 104 passed (new cases: post target round-trip, article slug resolution, cascade on target delete)
  • pnpm test:e2e — 317 passed (new tests/e2e/notification/deep-links.test.ts drives a real post like, comment and comment like, then asserts the payload)
  • tsc -p tsconfig.build.json --noEmit, pnpm lint, prettier — clean

The test database was reset with the user's explicit consent to apply and verify the migration.

Not in this PR

Findings 6-14 of the audit, notably: unread-count endpoint and per-notification read (6-7, next PR), notification cleanup on unlike/unfollow (8), realtime emit inside the transaction (9), one socket per user in WebSocketManager (11), and the wrong default-avatar.png fallback in the mapper (12).

🤖 Generated with Claude Code

https://claude.ai/code/session_01PwzkQ5YGFXSB9jWCZKzX4H

…ation

Tapping a notification could not open what it was about. Post likes and
comment likes persisted no referenceId at all, so the stored row had no
destination even though the realtime event carried one. Comment
notifications stored only the comment id, leaving the client to resolve
the post or article it lives under. Post and article likes shared the
LIKE type with nothing to tell them apart, articles are read by slug
while notifications only knew their uuid, and the notification id was
never mapped out of Prisma, so a single notification could not be
addressed at all.

Notifications now store postId / articleId / commentId next to
referenceId, which the entity derives from the target: the comment,
else the article, else the post. Article slugs are resolved on read
rather than denormalised, since a slug follows its title. The columns
are real foreign keys with ON DELETE CASCADE, so a notification cannot
outlive what it points at. The response exposes the notification id
along with the target ids, and the realtime payload carries the article
slug so both paths can build the same URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwzkQ5YGFXSB9jWCZKzX4H
@aquie00t
aquie00t merged commit de8fbe3 into main Aug 26, 2026
10 checks passed
@aquie00t
aquie00t deleted the feat/notification-deep-links branch August 26, 2026 21:49
github-actions Bot pushed a commit that referenced this pull request Aug 26, 2026
# [1.8.0](v1.7.4...v1.8.0) (2026-08-26)

### Features

* **notification:** carry the whole deep-link target on every notification ([#221](#221)) ([de8fbe3](de8fbe3))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.8.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