Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/clients/conversations-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { http, HttpResponse } from 'msw'
import { describe, expect, it } from 'vitest'

import { CommsApi } from '../comms-api'
import { server } from '../testUtils/msw-setup'
import {
TEST_API_BASE_URL as BASE,
TEST_API_TOKEN,
TEST_CONVERSATION_ID,
} from '../testUtils/test-defaults'

// Pins the wire shape of the `conversations/get` pagination args. `olderThan`
// is a `Date` that the client must convert itself — the transport's generic
// snake-casing would turn a `Date` into an empty object.

describe('ConversationsClient — wire serialization', () => {
async function captureGetParams(
args: Parameters<CommsApi['conversations']['getConversations']>[0],
) {
const capturedUrls: URL[] = []
server.use(
http.get(`${BASE}/conversations/get`, ({ request }) => {
capturedUrls.push(new URL(request.url))
return HttpResponse.json([])
}),
)

const api = new CommsApi(TEST_API_TOKEN)
await api.conversations.getConversations(args)

expect(capturedUrls).toHaveLength(1)
return (capturedUrls[0] as URL).searchParams
}

it('getConversations sends the compound cursor as older_than_ts / before_id', async () => {
const olderThan = new Date('2026-06-25T15:53:53Z')
const params = await captureGetParams({
workspaceId: 123,
archived: false,
olderThan,
beforeId: TEST_CONVERSATION_ID,
limit: 500,
})

expect(params.get('workspace_id')).toBe('123')
expect(params.get('archived')).toBe('false')
expect(params.get('older_than_ts')).toBe(String(Math.floor(olderThan.getTime() / 1000)))
// The Date arg must not leak through the generic snake-casing.
expect(params.has('older_than')).toBe(false)
expect(params.get('before_id')).toBe(TEST_CONVERSATION_ID)
expect(params.get('limit')).toBe('500')
})

it('getConversations keeps the minimal call shape unchanged', async () => {
const params = await captureGetParams({ workspaceId: 123 })

expect(params.get('workspace_id')).toBe('123')
expect(params.has('archived')).toBe(false)
expect(params.has('older_than_ts')).toBe(false)
expect(params.has('before_id')).toBe(false)
expect(params.has('limit')).toBe(false)
})
})
36 changes: 31 additions & 5 deletions src/clients/conversations-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,52 @@ export class ConversationsClient extends BaseClient {
: ConversationListSchema

/**
* Gets all conversations for a workspace.
* Gets a page of conversations for a workspace, newest activity first.
* The server returns at most 500 rows per request (20 by default); pass
* the last row's `lastActive`/`id` as `olderThan`/`beforeId` to fetch
* the next page. Paired, they form a strict compound boundary, so pages
* never repeat rows. Omitting `archived` returns active and archived
* conversations mixed.
*
* @param args - The arguments for getting conversations.
* @param args.workspaceId - The workspace ID.
* @param args.archived - Optional flag to include archived conversations.
* @param args.archived - Optional flag to filter archived (true) or active (false) conversations.
* @param args.olderThan - Optional date to get conversations last active before.
* @param args.beforeId - Optional conversation id. Paired with olderThan it forms the
* strict compound cursor; alone it pages by conversation id order instead.
* @param args.limit - Optional page size (server default 20, max 500).
* @returns An array of conversation objects.
*
* @example
* ```typescript
* const conversations = await api.conversations.getConversations({ workspaceId: 123 })
* conversations.forEach(c => console.log(c.title))
* const page = await api.conversations.getConversations({ workspaceId: 123, limit: 500 })
* const last = page.at(-1)
* const nextPage = last
* ? await api.conversations.getConversations({
* workspaceId: 123,
* limit: 500,
* olderThan: last.lastActive,
* beforeId: last.id,
* })
* : []
* ```
*/
getConversations(args: GetConversationsArgs): Promise<Conversation[]> {
// Fields are picked explicitly (matching getThreads/getComments) so a
// future Date field can't silently reach the generic snake-casing,
// which would turn it into an empty object on the wire.
const params: Record<string, unknown> = { workspaceId: args.workspaceId }
if (args.archived != null) params.archived = args.archived
if (args.olderThan) params.olderThanTs = Math.floor(args.olderThan.getTime() / 1000)
if (args.beforeId != null) params.beforeId = args.beforeId
if (args.limit != null) params.limit = args.limit

return request<Conversation[]>({
httpMethod: 'GET',
baseUri: this.getBaseUri(),
relativePath: `${ENDPOINT_CONVERSATIONS}/get`,
apiToken: this.apiToken,
payload: args,
payload: params,
customFetch: this.customFetch,
}).then((response) => this.conversationListSchema.parse(response.data))
}
Expand Down
3 changes: 3 additions & 0 deletions src/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ export type GetCommentsArgs = z.infer<typeof GetCommentsArgsSchema>
export const GetConversationsArgsSchema = z.object({
workspaceId: z.number(),
archived: z.boolean().nullable().optional(),
olderThan: z.date().nullable().optional(),
beforeId: z.string().nullable().optional(),
Comment thread
amix marked this conversation as resolved.
Comment thread
amix marked this conversation as resolved.
limit: z.number().nullable().optional(),
})

export type GetConversationsArgs = z.infer<typeof GetConversationsArgsSchema>
Expand Down
Loading