Issue: GAR-765
Slice: Chats slice 12 / /v1/me namespace extension
Branch: routine/202605311818-me-chats-inbox
Estimativa: ~200 LOC, ~2h
Add GET /v1/me/chats — a cursor-paginated inbox returning all non-archived chats where the
authenticated caller holds a chat_members row, scoped to a specific group.
Follows the established pattern of GET /v1/me/mentions (plan 0237) and
GET /v1/me/tasks (plan 0242).
GET /v1/me/chats?group_id=<uuid>[&after=<chat_id>&limit=<n>&type=channel|dm|thread]
│
├── Principal extractor → user_id
├── BEGIN tx (app pool)
├── SET LOCAL app.current_user_id
├── SET LOCAL app.current_group_id
├── SELECT c.id, c.group_id, c.name, c.type, cm.role, cm.joined_at,
│ cm.muted, cm.last_read_at, c.archived_at
│ FROM chat_members cm
│ JOIN chats c ON cm.chat_id = c.id
│ WHERE cm.user_id = $1
│ AND c.group_id = $2
│ AND c.archived_at IS NULL
│ [AND c.type = $3]
│ [cursor: AND (cm.joined_at, cm.chat_id) < (subquery)]
│ ORDER BY cm.joined_at DESC, cm.chat_id DESC
│ LIMIT $N
├── COMMIT
└── 200 JSON { items, next_cursor }
- Rust / Axum 0.8 (
garraia-gateway) sqlxasync Postgres (non-macroquery_as— consistent with plan 0242 pattern)- utoipa OpenAPI annotations
- No new migration —
chats+chat_membersfrom migration 004; RLS from migration 007
- RLS enforced in-tx:
SET LOCAL app.current_user_id+SET LOCAL app.current_group_idboth required before any query —chats_group_isolation+chat_members_through_chats(migration 007) filter automatically. - group_id required: same as tasks/mentions — can't omit because RLS needs it.
- Non-archived only:
c.archived_at IS NULLby default; no flag to include archived. - Keyset cursor on (joined_at DESC, chat_id DESC): stable even under concurrent inserts.
- No PII in cursor: cursor is just the
chat_idUUID of the last item. - type filter validated: accepted values:
channel,dm,thread. Any other → 400. - No unwrap() in production code.
- No SQL string concat: all params via
.bind().
-
chat_memberstable hasuser_id,chat_id,role,joined_at,muted,last_read_at(migration 004) -
chatstable hasid,group_id,name,type,archived_at(migration 004) - RLS policies
chats_group_isolation+chat_members_through_chatsexist (migration 007) - Pattern established in
me.rsvialist_my_tasks+list_my_mentions - No competing issue found in Linear search
- Including archived chats (separate slice if ever needed)
- Unread count (requires counting messages vs
last_read_at— deferred) - Cross-group chat list (blocked by RLS needing
app.current_group_id) - WebSocket / SSE stream of chat updates
PR is non-destructive (additive handler + route). Rolling back means removing the route from
mod.rs and the handler block from me.rs. No migration to reverse.
None — pattern is fully established.
crates/garraia-gateway/src/rest_v1/
me.rs ← add ListMyChatsQuery, ChatMembershipSummary, MyChatsMembershipResponse,
list_my_chats handler
mod.rs ← add .route("/v1/me/chats", get(me::list_my_chats))
- T1 — Write unit tests for
list_my_chats(query validation, type filter, cursor) - T2 — Implement
ListMyChatsQuery,ChatMembershipSummary,MyChatsMembershipResponse - T3 — Implement
list_my_chatshandler with 4-branch cursor×type query - T4 — Register route
/v1/me/chatsinmod.rs - T5 —
cargo clippyclean;cargo test -p garraia-gatewaygreen - T6 — Update
ROADMAP.md§3.4 chats + §3.6 checklist - T7 — Update
plans/README.mdrow - T8 — Update
TODO.md
| Risk | Likelihood | Mitigation |
|---|---|---|
| RLS policy returns 0 rows if group_id wrong | Low | Unit test with cross-group guard |
| Cursor subquery returns NULL → safe empty result | Covered | Same pattern as list_my_tasks |
| Type filter SQL injection | None | Always via .bind() with validation guard |
GET /v1/me/chats?group_id=<valid>→ 200 with caller's chats in that group, newest-joined first.?type=channelfilters to channels only;?type=invalid→ 400.- Cursor pagination:
?after=<chat_id>returns the next page correctly. - Caller NOT in group → empty list (RLS returns 0 rows; not a 403 — consistent with tasks/mentions).
- Archived chats excluded.
- 7+ unit tests green.
- Plan 0237 (GAR-755) —
GET /v1/me/mentions(model) - Plan 0242 (GAR-763) —
GET /v1/me/tasks(model) - Migration 004 —
chats+chat_membersschema - Migration 007 — RLS policies (
chats_group_isolation,chat_members_through_chats) - ROADMAP §3.4 Chats + §3.6 Chat compartilhado
Created 2026-05-31 (America/New_York)