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
19 changes: 18 additions & 1 deletion src/clients/add-comment-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { describe, expect, it } from 'vitest'
import { getCommsBaseUri } from '../consts/endpoints'
import { server } from '../testUtils/msw-setup'
import { TEST_API_BASE_URL, TEST_API_TOKEN, TEST_THREAD_ID } from '../testUtils/test-defaults'
import { CommentSchema, createCommentSchema } from '../types/entities'
import { EVERYONE, EVERYONE_IN_THREAD } from '../types/enums'
import { addCommentRequest } from './add-comment-helper'

const ctx = { baseUri: getCommsBaseUri(), apiToken: TEST_API_TOKEN }
const ctx = { baseUri: getCommsBaseUri(), apiToken: TEST_API_TOKEN, schema: CommentSchema }
Comment thread
scottlovegrove marked this conversation as resolved.
const COMMENT_ADD = `${TEST_API_BASE_URL}/comments/add`

const COMMENT_RESPONSE = {
Expand All @@ -20,6 +21,22 @@ const COMMENT_RESPONSE = {
system_message: null,
}

describe('addCommentRequest — base-bound schema', () => {
it('roots the returned comment url at the schema base (close/reopen path)', async () => {
const customBase = 'https://comms.example.com'
server.use(http.post(COMMENT_ADD, () => HttpResponse.json(COMMENT_RESPONSE)))

const comment = await addCommentRequest(
{ ...ctx, schema: createCommentSchema(customBase) },
{ threadId: TEST_THREAD_ID, content: 'hello' },
)

expect(comment.url).toBe(
`${customBase}/a/1/ch/BBBBBBBBBBBBBBBBBBBBBB/t/${TEST_THREAD_ID}/c/AAAAAAAAAAAAAAAAAAAAAA`,
)
})
})

describe('addCommentRequest — reserved broadcast marker validation', () => {
it('throws when a marker is passed in `groups`', () => {
expect(() =>
Expand Down
6 changes: 4 additions & 2 deletions src/clients/add-comment-helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ENDPOINT_COMMENTS } from '../consts/endpoints'
import { request } from '../transport/http-client'
import { type Comment, CommentSchema } from '../types/entities'
import { type Comment, type createCommentSchema } from '../types/entities'
import { NOTIFY_AUDIENCE_GROUP_IDS, NOTIFY_AUDIENCES, type NotifyAudience } from '../types/enums'
import type { CustomFetch } from '../types/http'
import type { CreateCommentArgs, ThreadAction } from '../types/requests'
Expand All @@ -10,6 +10,8 @@ type ClientContext = {
baseUri: string
apiToken: string
customFetch?: CustomFetch
/** Per-client Comment schema, base-bound for the returned comment's web `url`. */
schema: ReturnType<typeof createCommentSchema>
}

const SENTINEL_GROUP_IDS: ReadonlySet<string> = new Set(Object.values(NOTIFY_AUDIENCE_GROUP_IDS))
Expand Down Expand Up @@ -90,5 +92,5 @@ export function addCommentRequest(
apiToken: context.apiToken,
payload,
customFetch: context.customFetch,
}).then((response) => CommentSchema.parse(response.data))
}).then((response) => context.schema.parse(response.data))
}
9 changes: 9 additions & 0 deletions src/clients/base-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,13 @@ export class BaseClient {
protected getBaseUri(): string {
return getCommsBaseUri(this.defaultVersion, this.baseUrl)
}

/**
* Base URL for entity web links, or `undefined` to use getFullCommsURL's
* default web app. Trailing-slash normalization happens in
* `getFullCommsURL`, so the configured value is returned verbatim.
*/
protected getLinkBaseUrl(): string | undefined {
return this.baseUrl
}
}
38 changes: 29 additions & 9 deletions src/clients/channels-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { z } from 'zod'
import { ENDPOINT_CHANNELS } from '../consts/endpoints'
import { request } from '../transport/http-client'
import { type Channel, ChannelSchema, type StatusOk, StatusOkSchema } from '../types/entities'
import {
type Channel,
ChannelSchema,
createChannelSchema,
type StatusOk,
StatusOkSchema,
} from '../types/entities'
import type {
AddChannelUserArgs,
AddChannelUsersArgs,
Expand All @@ -22,6 +28,15 @@ export const ChannelListSchema = z.array(ChannelSchema)
* to keep an optimistic-UI ID stable through the round-trip.
*/
export class ChannelsClient extends BaseClient {
private readonly linkBaseUrl = this.getLinkBaseUrl()
// Reuse the shared singletons when no custom base is configured.
private readonly channelSchema = this.linkBaseUrl
? createChannelSchema(this.linkBaseUrl)
: ChannelSchema
private readonly channelListSchema = this.linkBaseUrl
? z.array(this.channelSchema)
: ChannelListSchema

/**
* Gets all channels for a given workspace.
*
Expand All @@ -44,7 +59,7 @@ export class ChannelsClient extends BaseClient {
apiToken: this.apiToken,
payload: args,
customFetch: this.customFetch,
}).then((response) => ChannelListSchema.parse(response.data))
}).then((response) => this.channelListSchema.parse(response.data))
}

/**
Expand All @@ -54,7 +69,7 @@ export class ChannelsClient extends BaseClient {
* @returns The channel object.
*/
getChannel(id: string): Promise<Channel> {
return this.simple('GET', 'getone', { id }, ChannelSchema)
return this.simple('GET', 'getone', { id }, this.channelSchema)
}

/**
Expand All @@ -80,7 +95,12 @@ export class ChannelsClient extends BaseClient {
* ```
*/
createChannel(args: CreateChannelArgs): Promise<Channel> {
return this.simple('POST', 'add', { ...args, id: resolveCreateId(args.id) }, ChannelSchema)
return this.simple(
'POST',
'add',
{ ...args, id: resolveCreateId(args.id) },
this.channelSchema,
)
}

/**
Expand All @@ -95,7 +115,7 @@ export class ChannelsClient extends BaseClient {
* @returns The updated channel object.
*/
updateChannel(args: UpdateChannelArgs): Promise<Channel> {
return this.simple('POST', 'update', { ...args }, ChannelSchema)
return this.simple('POST', 'update', { ...args }, this.channelSchema)
}

/**
Expand Down Expand Up @@ -170,7 +190,7 @@ export class ChannelsClient extends BaseClient {
* ```
*/
addUser(args: AddChannelUserArgs): Promise<Channel> {
return this.simple('POST', 'add_user', { ...args }, ChannelSchema)
return this.simple('POST', 'add_user', { ...args }, this.channelSchema)
}

/**
Expand All @@ -186,7 +206,7 @@ export class ChannelsClient extends BaseClient {
* ```
*/
addUsers(args: AddChannelUsersArgs): Promise<Channel> {
return this.simple('POST', 'add_users', { ...args }, ChannelSchema)
return this.simple('POST', 'add_users', { ...args }, this.channelSchema)
}

/**
Expand All @@ -197,7 +217,7 @@ export class ChannelsClient extends BaseClient {
* @param args.userId - The user ID to remove.
*/
removeUser(args: RemoveChannelUserArgs): Promise<Channel> {
return this.simple('POST', 'remove_user', { ...args }, ChannelSchema)
return this.simple('POST', 'remove_user', { ...args }, this.channelSchema)
}

/**
Expand All @@ -208,7 +228,7 @@ export class ChannelsClient extends BaseClient {
* @param args.userIds - Array of user IDs to remove.
*/
removeUsers(args: RemoveChannelUsersArgs): Promise<Channel> {
return this.simple('POST', 'remove_users', { ...args }, ChannelSchema)
return this.simple('POST', 'remove_users', { ...args }, this.channelSchema)
}

private simple<T>(
Expand Down
61 changes: 61 additions & 0 deletions src/clients/comments-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,64 @@ describe('CommentsClient — wire serialization', () => {
})
})
})

describe('CommentsClient — baseUrl in entity links', () => {
it('roots the returned comment url at the configured baseUrl', async () => {
const customBase = 'https://comms.example.com'
const responseChannelId = '7YpL3oZ4kZ9vP7Q1tR2sX3y'
const responseCommentId = '7YpL3oZ4kZ9vP7Q1tR2sX41'
server.use(
http.post(`${customBase}/api/v1/comments/add`, () =>
HttpResponse.json({
id: responseCommentId,
thread_id: TEST_THREAD_ID,
channel_id: responseChannelId,
creator: 1,
content: 'hello',
posted_ts: Math.floor(Date.now() / 1000),
workspace_id: 1,
system_message: null,
}),
),
)

const api = new CommsApi(TEST_API_TOKEN, { baseUrl: customBase })
const comment = await api.comments.createComment({
threadId: TEST_THREAD_ID,
content: 'hello',
})

expect(comment.url).toBe(
`${customBase}/a/1/ch/${responseChannelId}/t/${TEST_THREAD_ID}/c/${responseCommentId}`,
)
})

it('strips a trailing slash on baseUrl so links do not double up', async () => {
const responseChannelId = '7YpL3oZ4kZ9vP7Q1tR2sX3y'
const responseCommentId = '7YpL3oZ4kZ9vP7Q1tR2sX41'
server.use(
http.post('https://comms.example.com/api/v1/comments/add', () =>
HttpResponse.json({
id: responseCommentId,
thread_id: TEST_THREAD_ID,
channel_id: responseChannelId,
creator: 1,
content: 'hello',
posted_ts: Math.floor(Date.now() / 1000),
workspace_id: 1,
system_message: null,
}),
),
)

const api = new CommsApi(TEST_API_TOKEN, { baseUrl: 'https://comms.example.com/' })
const comment = await api.comments.createComment({
threadId: TEST_THREAD_ID,
content: 'hello',
})

expect(comment.url).toBe(
`https://comms.example.com/a/1/ch/${responseChannelId}/t/${TEST_THREAD_ID}/c/${responseCommentId}`,
)
})
})
35 changes: 29 additions & 6 deletions src/clients/comments-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { z } from 'zod'
import { ENDPOINT_COMMENTS } from '../consts/endpoints'
import { request } from '../transport/http-client'
import { type Comment, CommentSchema, type StatusOk, StatusOkSchema } from '../types/entities'
import {
type Comment,
CommentSchema,
createCommentSchema,
type StatusOk,
StatusOkSchema,
} from '../types/entities'
import type {
CreateCommentArgs,
GetCommentsArgs,
Expand All @@ -18,6 +24,19 @@ export const CommentListSchema = z.array(CommentSchema)
* on `createComment` when the caller doesn't supply one.
*/
export class CommentsClient extends BaseClient {
private readonly linkBaseUrl = this.getLinkBaseUrl()
// Reuse the shared singletons when no custom base is configured.
private readonly commentSchema = this.linkBaseUrl
? createCommentSchema(this.linkBaseUrl)
: CommentSchema
private readonly commentListSchema = this.linkBaseUrl
? z.array(this.commentSchema)
: CommentListSchema
// `getone` wraps the comment in `{ comment: ... }`; built once per client.
private readonly wrappedCommentSchema = z
.object({ comment: this.commentSchema })
.transform((data) => data.comment)

/**
* Gets all comments for a thread. `newerThan` / `olderThan` (`Date`) are
* converted to `newer_than_ts` / `older_than_ts` epoch seconds on the
Expand Down Expand Up @@ -52,7 +71,7 @@ export class CommentsClient extends BaseClient {
apiToken: this.apiToken,
payload: params,
customFetch: this.customFetch,
}).then((response) => CommentListSchema.parse(response.data))
}).then((response) => this.commentListSchema.parse(response.data))
}

/**
Expand All @@ -62,15 +81,14 @@ export class CommentsClient extends BaseClient {
* @returns The comment object.
*/
getComment(id: string): Promise<Comment> {
const wrappedSchema = z.object({ comment: CommentSchema }).transform((data) => data.comment)
return request<Comment>({
httpMethod: 'GET',
baseUri: this.getBaseUri(),
relativePath: `${ENDPOINT_COMMENTS}/getone`,
apiToken: this.apiToken,
payload: { id },
customFetch: this.customFetch,
}).then((response) => wrappedSchema.parse(response.data))
}).then((response) => this.wrappedCommentSchema.parse(response.data))
}

/**
Expand Down Expand Up @@ -102,7 +120,12 @@ export class CommentsClient extends BaseClient {
*/
createComment(args: CreateCommentArgs): Promise<Comment> {
return addCommentRequest(
{ baseUri: this.getBaseUri(), apiToken: this.apiToken, customFetch: this.customFetch },
{
baseUri: this.getBaseUri(),
apiToken: this.apiToken,
customFetch: this.customFetch,
schema: this.commentSchema,
},
args,
)
}
Expand All @@ -123,7 +146,7 @@ export class CommentsClient extends BaseClient {
apiToken: this.apiToken,
payload: { ...args },
customFetch: this.customFetch,
}).then((response) => CommentSchema.parse(response.data))
}).then((response) => this.commentSchema.parse(response.data))
}

/**
Expand Down
18 changes: 14 additions & 4 deletions src/clients/conversation-messages-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { request } from '../transport/http-client'
import {
type ConversationMessage,
ConversationMessageSchema,
createConversationMessageSchema,
type StatusOk,
StatusOkSchema,
} from '../types/entities'
Expand All @@ -22,6 +23,15 @@ export const ConversationMessageListSchema = z.array(ConversationMessageSchema)
* message `id` on `createMessage` when the caller doesn't supply one.
*/
export class ConversationMessagesClient extends BaseClient {
private readonly linkBaseUrl = this.getLinkBaseUrl()
// Reuse the shared singletons when no custom base is configured.
private readonly messageSchema = this.linkBaseUrl
? createConversationMessageSchema(this.linkBaseUrl)
: ConversationMessageSchema
private readonly messageListSchema = this.linkBaseUrl
? z.array(this.messageSchema)
: ConversationMessageListSchema

/**
* Gets all messages in a conversation.
*
Expand Down Expand Up @@ -55,7 +65,7 @@ export class ConversationMessagesClient extends BaseClient {
apiToken: this.apiToken,
payload: params,
customFetch: this.customFetch,
}).then((response) => ConversationMessageListSchema.parse(response.data))
}).then((response) => this.messageListSchema.parse(response.data))
}

/**
Expand All @@ -70,7 +80,7 @@ export class ConversationMessagesClient extends BaseClient {
* ```
*/
getMessage(id: string): Promise<ConversationMessage> {
return this.simple('GET', 'getone', { id }, ConversationMessageSchema)
return this.simple('GET', 'getone', { id }, this.messageSchema)
}

/**
Expand Down Expand Up @@ -104,7 +114,7 @@ export class ConversationMessagesClient extends BaseClient {
if (args.directGroupMentions) params.directGroupMentions = args.directGroupMentions
if (args.notify !== undefined) params.notify = args.notify

return this.simple('POST', 'add', params, ConversationMessageSchema)
return this.simple('POST', 'add', params, this.messageSchema)
}

/**
Expand All @@ -131,7 +141,7 @@ export class ConversationMessagesClient extends BaseClient {
if (args.directMentions) params.directMentions = args.directMentions
if (args.directGroupMentions) params.directGroupMentions = args.directGroupMentions

return this.simple('POST', 'update', params, ConversationMessageSchema)
return this.simple('POST', 'update', params, this.messageSchema)
}

/**
Expand Down
Loading
Loading