docs: FeedEntry decoupling design spec
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
# FeedEntry Decoupling Design
|
||||||
|
|
||||||
|
**Goal:** Fix search viewer context (functional), restructure `FeedEntry` for clarity (structural), and make viewer presence explicit via `Option<ViewerContext>` (type-safe).
|
||||||
|
|
||||||
|
**Priority:** C (search fix) → B (struct clarity) → A (type safety). All three land in one pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
Replace flat fields on `FeedEntry` with two named sub-structs in `crates/domain/src/models/feed.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EngagementStats {
|
||||||
|
pub like_count: i64,
|
||||||
|
pub boost_count: i64,
|
||||||
|
pub reply_count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ViewerContext {
|
||||||
|
pub liked: bool,
|
||||||
|
pub boosted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FeedEntry {
|
||||||
|
pub thought: Thought,
|
||||||
|
pub author: User,
|
||||||
|
pub stats: EngagementStats,
|
||||||
|
pub viewer: Option<ViewerContext>, // None when no authenticated viewer
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`viewer: None` means the request was anonymous or viewer state is unavailable (e.g. search without auth). `viewer: Some(ViewerContext { liked: false, boosted: false })` means a viewer is known and they have not liked or boosted the thought. These two states are now distinct at the type level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Search Adapter Fix
|
||||||
|
|
||||||
|
`SearchPort::search_thoughts` already accepts `viewer_id: Option<&UserId>` but `postgres-search/src/lib.rs` ignores it, always hardcoding `false` for viewer fields.
|
||||||
|
|
||||||
|
Fix: conditionally inject EXISTS subqueries into the search SQL, identical to the pattern used in `postgres/src/feed.rs`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- viewer_id = None (anonymous)
|
||||||
|
false AS liked_by_viewer,
|
||||||
|
false AS boosted_by_viewer
|
||||||
|
|
||||||
|
-- viewer_id = Some(uid)
|
||||||
|
EXISTS(SELECT 1 FROM likes WHERE user_id='{uid}' AND thought_id=t.id) AS liked_by_viewer,
|
||||||
|
EXISTS(SELECT 1 FROM boosts WHERE user_id='{uid}' AND thought_id=t.id) AS boosted_by_viewer
|
||||||
|
```
|
||||||
|
|
||||||
|
The `FeedRow` struct in postgres-search already has `liked_by_viewer: bool` and `boosted_by_viewer: bool` columns — they just need to be populated correctly. No schema change required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Callsite Migration
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `crates/domain/src/models/feed.rs` | Replace flat stats/viewer fields with `EngagementStats` and `Option<ViewerContext>` |
|
||||||
|
| `crates/adapters/postgres/src/feed.rs` — `row_to_entry` | Construct `EngagementStats { ... }` and `viewer: Some/None` based on `FeedRow` |
|
||||||
|
| `crates/adapters/postgres-search/src/lib.rs` — `row_to_entry` + SQL | Fix SQL to use viewer_id; build `Option<ViewerContext>` from result |
|
||||||
|
| `crates/presentation/src/handlers/feed.rs` — `to_thought_response` | `e.stats.like_count`, `e.viewer.as_ref().map(|v| v.liked).unwrap_or(false)` |
|
||||||
|
| `crates/domain/src/testing.rs` — `TestStore` feed impl | Build `FeedEntry` with `stats:` and `viewer:` fields |
|
||||||
|
|
||||||
|
`ThoughtResponse` in `api-types/src/responses.rs` keeps `liked_by_viewer: bool` and `boosted_by_viewer: bool` — the wire format is unchanged. `viewer: None` serialises as `false` in `to_thought_response`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Does Not Change
|
||||||
|
|
||||||
|
- `FeedRepository` port signatures (still returns `Paginated<FeedEntry>`)
|
||||||
|
- HTTP response shape (`ThoughtResponse`)
|
||||||
|
- Database schema
|
||||||
|
- Pagination, filtering, or query logic
|
||||||
|
- Any code path that doesn't touch `FeedEntry` fields directly
|
||||||
Reference in New Issue
Block a user