Skip to content

Feature/poly 12 inbox query - #25

Closed
Taye-Staats wants to merge 7 commits into
devfrom
feature/POLY-12-inbox-query
Closed

Taye-Staats wants to merge 7 commits into
devfrom
feature/POLY-12-inbox-query

Conversation

@Taye-Staats

@Taye-Staats Taye-Staats commented Jan 25, 2026

Copy link
Copy Markdown
Collaborator

Linked Issues

Closes # Sub Issue 6
Linear: (e.g., POLY-12)

Summary

Briefly explain the change and why.
Add listUserConversations() in backend/convex/messages.ts
Added Security to make sure the users can only access there messages
Calculates unread count
Shows last user message

How to Test

Steps to verify locally:

  • npm run lint
  • npm run typecheck
  • npm test
  • Manual flow:
    1. npm run dev:backend (in terminal A)
    2. npm run dev (in terminal B)
    3. Verify the change: <describe expected behavior/screens>

Checklist

  • Tests added/updated (if applicable)
  • Lint/tests pass locally (npm run lint)
  • Docs updated (README/ADR/changelog if needed)
  • Follows conventional commit format
  • No merge conflicts with dev

Screenshots / Demos

(if UI or visible behavior - attach images, videos, or GIFs)

Summary by CodeRabbit

  • New Features

    • Messaging system: send messages, view conversations with last-message previews, unread counts, and cursored paging
    • User profiles surfaced in messaging
  • Tests

    • Added conversation-listing tests covering filtering, auth, and unread-count logic
    • Deterministic server mocks for reliable test behavior
  • Chores

    • Updated test/build tooling and dev dependencies (Jest/TypeScript/Babel)

@coderabbitai

coderabbitai Bot commented Jan 25, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a Convex-backed messaging feature (schema, mutations/queries, handler), tests and a Convex server mock, and updates Jest/Babel/TypeScript configs to enable backend testing with ESM/TypeScript support.

Changes

Cohort / File(s) Summary
Testing infra & configs
backend/jest.config.js, jest.config.js, babel.config.js, package.json, backend/package.json, backend/convex/tsconfig.json
Adds/updates Jest and Babel configs, enables Jest types in backend TS config, adds test script and @types/jest, and adds @babel/preset-typescript. Adjusts transforms to support ESM and convex-test.
Mock implementation
backend/convex/__mocks__/server.js
Adds a Convex server mock exporting query, mutation, action, internalQuery, internalMutation, internalAction, and httpAction that call an optional handler or return the provided config for deterministic tests.
Messaging feature
backend/convex/messages.ts, backend/convex/__tests__/messages.test.ts
Implements sendMessage mutation and listUserConversations query plus listUserConversationsHandler. Adds tests covering auth, participant filtering, unread counts, previews, pagination, and empty results.
Database schema
backend/convex/schema.ts
Adds conversations, messages, conversationParticipants, and users tables with indices (by_listing_buyer_seller, by_updatedAt, by_conversation, by_user_lastActivityAt, by_conversationId, by_clerkId) and fields for messaging, participants, and user profiles.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Convex as ConvexHandler
  participant DB as ConvexDB

  Client->>Convex: listUserConversations(limit, cursor)
  Convex->>DB: query conversationParticipants by_user_lastActivityAt (userId, cursor, limit)
  DB-->>Convex: participantRows[]
  loop per participantRow
    Convex->>DB: query conversations by id
    DB-->>Convex: conversation
    Convex->>DB: query messages by_conversation (limit=1 desc)
    DB-->>Convex: lastMessage
    Convex->>DB: read participantRow.unreadCount or compute unread messages
    DB-->>Convex: unreadCount
    Convex-->>Client: conversation preview (otherUserId, lastMessagePreview, unreadCount, lastMessageAt, ...)
  end
  alt more results
    Convex-->>Client: nextCursor
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • jaydonkc

Poem

🐰 I hopped through schema, tests, and thread,

Mocked a server, counted unread,
Messages stitched and pages rolled,
Conversations warm and bold,
Hooray—new chats for all to spread!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feature/poly 12 inbox query' is partially related to the changeset, referring to inbox/conversation querying but using vague phrasing like 'poly 12' (ticket reference) without clearly describing the primary change. Revise title to be more specific and descriptive, e.g., 'Add listUserConversations query for inbox management' or 'Implement user conversation listing with unread counts'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description covers most required sections from the template including summary, how to test, and checklist, though the summary lacks specific detail and the testing checklist items remain unchecked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/POLY-12-inbox-query

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
backend/convex/__tests__/messages.test.ts (2)

57-134: Consider adding a test for cursor-based pagination.

The handler has non-trivial cursor logic (parsing, lt filtering, nextCursor computation from take(limit + 1)), but none of the four tests exercise it. A test with limit: 1 and multiple participant rows would cover the nextCursor return path and the cursor-filtering branch.


21-33: Mock silently swallows an invalid cursor by returning null from lt.

When lt() is called (cursor path), it returns null (Line 30). The enclosing withIndex ignores this return and always uses the captured state to filter. This works today but is fragile — if the handler code ever inspects the return of the filter-builder chain, the mock will silently diverge. Consider returning this or the same chainable object for consistency.

backend/convex/schema.ts (2)

48-55: type field is an unconstrained string — consider using a union literal.

The messages table defines type: v.string(), but the sendMessage mutation always sets type: 'text'. If you plan to support additional message types (e.g., image, system), a v.union(v.literal('text'), ...) now would prevent invalid values and make the schema self-documenting.


35-65: Dual participation tracking: participantIds array on conversations + conversationParticipants table.

Both conversations.participantIds (Line 39) and the conversationParticipants table (Line 56) track who belongs to a conversation. sendMessage reads participantIds for auth checks but iterates participantIds to upsert into conversationParticipants. If these ever drift apart (e.g., a participant is added to one but not the other), authorization and inbox queries will disagree. Consider documenting which is the source of truth, or deriving one from the other.

backend/convex/messages.ts (3)

90-90: args.limit || 20 treats 0 as falsy — use ?? instead.

If a caller explicitly passes limit: 0, the || operator falls through to 20. Using args.limit ?? 20 would respect an explicit zero. Admittedly limit: 0 is an odd request, but ?? is the idiomatic nullish-coalescing choice here.

Proposed fix
-  const limit = args.limit || 20;
+  const limit = args.limit ?? 20;

55-76: No mechanism to reset unreadCount — it only ever increases.

sendMessage increments unreadCount for non-sender participants but there's no visible markAsRead mutation to reset it. Without one, the inbox will show an ever-growing unread badge. If this is planned for a follow-up, consider adding a TODO or tracking issue.

Would you like me to open an issue to track adding a markConversationRead mutation that resets unreadCount to 0 for the authenticated user's participant row?


80-166: Handler uses any throughout — consider narrowing types for maintainability.

listUserConversationsHandler types both ctx and args as any, which disables all type-checking inside the function body. Even if the handler is exported for testing purposes, you could define lightweight interfaces (e.g., ListConversationsCtx and ListConversationsArgs) to retain IntelliSense and catch regressions without coupling to Convex internals.

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@backend/convex/__mocks__/server.js`:
- Around line 4-27: The mock wrappers for query, mutation, and action drop
additional handler parameters by only accepting and forwarding a single "args"
argument; update each factory (query, mutation, action) to accept variadic
arguments (e.g., use (...args) in the returned async function) and call
config.handler with those same arguments (pass through all received args) when
config.handler exists, keeping the existing fallback return of config otherwise;
reference the factory names query/mutation/action and the config.handler check
to locate the changes.

In `@backend/convex/messages.ts`:
- Around line 84-90: The API declares a cursor in listUserConversations but the
handler listUserConversationsHandler ignores it and always returns nextCursor:
null; either implement cursor-based pagination or remove the param to avoid a
misleading API. Fix option A (implement): update listUserConversationsHandler to
accept args.cursor, use it to filter/seek results (e.g., fetch conversations
with lastMessageAt < cursor or > depending on sort), apply args.limit (+1 to
detect more), slice to limit, and set nextCursor to the last returned
conversation's lastMessageAt (or null if no more). Fix option B (remove): remove
the cursor arg from the listUserConversations args and from the handler
signature and ensure nextCursor remains omitted or always null. Ensure
references to cursor and nextCursor are consistent across listUserConversations
and listUserConversationsHandler.
- Around line 4-21: The sendMessage mutation currently trusts the
client-supplied senderId and doesn't update conversation metadata; change it to
use ctx.auth.getUserIdentity() to obtain the true sender, remove or ignore the
args.senderId value, fetch the conversation by args.conversationId and verify
that the authenticated user's id is in the conversation.participantIds (throw an
error if not), then insert the message using the authenticated id and after
insertion update the conversation row's lastMessageAt, lastMessageId and
updatedAt fields with the new message's timestamp and id; reference the
sendMessage handler, ctx.auth.getUserIdentity(), participantIds, and the
conversations' lastMessageAt/lastMessageId/updatedAt when making these changes.

In `@jest.config.js`:
- Around line 9-22: The PR clears Jest's default node_modules ignore by setting
testPathIgnorePatterns: [], which removes the safety boundary; restore the
default behavior by either removing the testPathIgnorePatterns line or setting
testPathIgnorePatterns to include '/node_modules/' so Jest ignores node_modules
again (aligning with coveragePathIgnorePatterns and existing testMatch). Locate
the testPathIgnorePatterns entry in jest.config.js and update or delete it
accordingly.
🧹 Nitpick comments (3)
backend/convex/schema.ts (1)

35-46: Add participant lookup indexes for conversation queries.

Lines 35–46 define buyerId/sellerId but there’s no index to query by either, which forces full scans in listUserConversations. Consider adding indexes like by_buyerId and by_sellerId (or a participant index if supported).

♻️ Suggested index additions
   conversations: defineTable({
     listingId: v.id('listings'),
     buyerId: v.string(),
     sellerId: v.string(),
     participantIds: v.array(v.string()),
     lastMessageAt: v.optional(v.number()),
     lastMessageId: v.optional(v.id('messages')),
     createdAt: v.number(),
     updatedAt: v.number(),
   })
     .index('by_listing_buyer_seller', ['listingId', 'buyerId', 'sellerId'])
+    .index('by_buyerId', ['buyerId'])
+    .index('by_sellerId', ['sellerId'])
     .index('by_updatedAt', ['updatedAt']),
backend/convex/messages.ts (1)

33-61: Avoid full scans and per‑conversation message collects.

Line 34 collects all conversations, then each conversation performs two message queries, including a full collect() for unread counting. This is O(N) + N×M and won’t scale. Prefer indexed queries by buyerId/sellerId (after adding indexes) and consider denormalizing lastMessagePreview / unreadCount at write time, or at least filtering unread messages server‑side before collecting.

backend/jest.config.js (1)

5-8: Escape the leading dot in the moduleNameMapper regex.
^./_generated/server$ matches any single character before the slash; escaping keeps it specific to ./_generated/server and avoids accidental matches.

♻️ Suggested change
-    '^./_generated/server$': '<rootDir>/convex/__mocks__/server.js',
+    '^\\./_generated/server$': '<rootDir>/convex/__mocks__/server.js',

Comment thread backend/convex/__mocks__/server.js
Comment thread backend/convex/messages.ts
Comment thread backend/convex/messages.ts
Comment thread jest.config.js Outdated
@jaydonkc jaydonkc assigned jaydonkc and unassigned jaydonkc Jan 27, 2026
@jaydonkc
jaydonkc self-requested a review January 27, 2026 20:52

@jaydonkc jaydonkc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please resolve the CodeRabbit warnings

@SamanSP1386 SamanSP1386 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testPathIgnorePatterns: [] removes Jest’s default /node_modules/ ignore — probably best to drop that line or keep the default for consistency.
Also, we’re trusting senderId from the client when sending messages. We should derive it from ctx.auth.getUserIdentity(), verify the user is in participantIds, and update conversation metadata so inbox ordering stays correct.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@backend/convex/messages.ts`:
- Around line 80-99: The loop over displayedConversations causes an N+1 query:
for each conversation you call ctx.db.query('messages') twice (producing lastMsg
and allMessages/unreadCount); fix by batching: collect all conversation IDs from
displayedConversations, run a single messages query to fetch relevant messages
(or at least the latest message per conversation and unread messages filtered by
senderId/read) and then compute lastMsg and unreadCount in-memory for each conv
(use the existing variables lastMsg/unreadCount names and the same
withIndex('by_conversation') pattern but with a single batched query), or
alternatively denormalize unreadCount onto the conversation document and update
it on send/read so the loop can read conv.unreadCount directly; update the code
around displayedConversations, lastMsg, allMessages, and unreadCount
accordingly.
- Around line 60-63: The current code does a full table scan via
ctx.db.query('conversations').collect() (allConversations -> conversations)
which loads every conversation into memory; instead create and use a Convex
index or a join table so filtering happens server-side: add an index on
participantIds (or create a conversation_participants table mapping
userId->conversationId), then query that index/table to fetch only conversations
for userId and perform the sort (by lastMessageAt || createdAt) in the DB query;
replace the use of ctx.db.query('conversations').collect() and subsequent
client-side .filter/.sort with a server-side indexed query that returns only the
needed conversation rows.
- Around line 66-71: The cursor parsing currently uses parseInt(args.cursor, 10)
without validating the result, so a non-numeric cursor yields NaN and breaks the
filter; update the code around parseInt(args.cursor, 10) (the cursorValue
variable) to validate the parsed value (e.g., const cursorValue =
Number.parseInt(args.cursor, 10); if (Number.isNaN(cursorValue)) { /* either
ignore the cursor filter or throw a clear validation error */ }) and only apply
the conversations = conversations.filter(...) when cursorValue is a valid
number, ensuring comparisons like (conv.lastMessageAt || conv.createdAt) <
cursorValue run with a numeric RHS.

In `@backend/jest.config.js`:
- Around line 6-9: The moduleNameMapper entry for the '^.+/_generated/server$'
pattern points to an incorrect path containing a duplicated backend/ prefix;
update the mapped path in backend/jest.config.js (the moduleNameMapper object)
so that '^.+/_generated/server$' resolves to the actual mock file at
'<rootDir>/convex/__mocks__/server.js' (remove the extra "backend/" segment) to
ensure Jest finds the mock.
🧹 Nitpick comments (3)
backend/convex/__mocks__/server.js (1)

28-31: Internal handler mocks don't wrap the handler like external ones do.

internalQuery, internalMutation, internalAction, and httpAction return config as-is rather than wrapping config.handler in an async function. This is fine if no tests currently exercise internal handlers, but it's worth noting the inconsistency for when they're needed later.

backend/convex/messages.ts (2)

50-50: Handler uses any for both ctx and args, losing all type safety.

Since this handler is exported and shared between the query definition and tests, consider typing it with Convex's QueryCtx and a proper args interface. This would catch misuse at compile time rather than at runtime.

Sketch
-export const listUserConversationsHandler = async (ctx: any, args: any) => {
+import { QueryCtx } from './_generated/server';
+
+interface ListUserConversationsArgs {
+  limit?: number;
+  cursor?: string;
+}
+
+export const listUserConversationsHandler = async (
+  ctx: QueryCtx,
+  args: ListUserConversationsArgs
+) => {

107-107: || fallback treats empty-string message body as "No messages yet".

If lastMsg.body is '', the || operator falls through to the default. Use ?? (nullish coalescing) if you want to preserve empty strings.

-      lastMessagePreview: lastMsg?.body || 'No messages yet',
+      lastMessagePreview: lastMsg?.body ?? 'No messages yet',

Comment thread backend/convex/messages.ts Outdated
Comment thread backend/convex/messages.ts
Comment thread backend/convex/messages.ts Outdated
Comment thread backend/jest.config.js

@SamanSP1386 SamanSP1386 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some potential issues, take a look at them.

@jaydonkc jaydonkc closed this Mar 2, 2026
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.

3 participants