fixed types on slack handlers

This commit is contained in:
dal 2025-07-09 14:34:02 -06:00
parent b179a7dd9d
commit b062694a2c
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
10 changed files with 103 additions and 54 deletions

View File

@ -1,9 +1,14 @@
import { getUserOrganizationId } from '@buster/database';
import {
type GetChannelsResponse,
type GetIntegrationResponse,
type InitiateOAuthResponse,
InitiateOAuthSchema,
OAuthCallbackSchema,
type RemoveIntegrationResponse,
SlackError,
SlackErrorCodes,
type SlackErrorResponse,
type UpdateIntegrationResponse,
UpdateIntegrationSchema,
} from '@buster/server-shared/slack';
import { SlackChannelService } from '@buster/slack';
@ -34,14 +39,14 @@ export class SlackHandler {
* POST /api/v2/slack/auth/init
* Initiate OAuth flow
*/
async initiateOAuth(c: Context) {
async initiateOAuth(c: Context): Promise<Response> {
try {
// Get service instance (lazy initialization)
const slackOAuthService = this.getSlackOAuthService();
// Check if service is available
if (!slackOAuthService) {
return c.json(
return c.json<SlackErrorResponse>(
{
error: 'Slack integration is not configured',
code: 'INTEGRATION_NOT_CONFIGURED',
@ -52,7 +57,7 @@ export class SlackHandler {
// Check if integration is enabled
if (!slackOAuthService.isEnabled()) {
return c.json(
return c.json<SlackErrorResponse>(
{
error: 'Slack integration is not enabled',
code: 'INTEGRATION_DISABLED',
@ -75,10 +80,10 @@ export class SlackHandler {
}
// Parse request body
const body = await c.req.json().catch(() => ({}));
const body: unknown = await c.req.json().catch(() => ({}));
const parsed = InitiateOAuthSchema.safeParse(body);
const metadata = parsed.success ? parsed.data.metadata : undefined;
const metadata = parsed.success ? parsed.data?.metadata : undefined;
// Add IP address to metadata
const enrichedMetadata = {
@ -93,7 +98,10 @@ export class SlackHandler {
metadata: enrichedMetadata,
});
return c.json(result);
return c.json<InitiateOAuthResponse>({
auth_url: result.authUrl,
state: result.state,
});
} catch (error) {
console.error('Failed to initiate OAuth:', error);
@ -121,7 +129,7 @@ export class SlackHandler {
* GET /api/v2/slack/auth/callback
* Handle OAuth callback from Slack
*/
async handleOAuthCallback(c: Context) {
async handleOAuthCallback(c: Context): Promise<Response> {
try {
// Get service instance (lazy initialization)
const slackOAuthService = this.getSlackOAuthService();
@ -132,7 +140,7 @@ export class SlackHandler {
}
// Parse query parameters
const query = c.req.query();
const query = c.req.query() as Record<string, string>;
console.info('OAuth callback received', {
hasCode: !!query.code,
hasState: !!query.state,
@ -149,7 +157,7 @@ export class SlackHandler {
}
console.error('Invalid OAuth callback parameters:', {
errors: parsed.error.errors,
errors: parsed.error.issues,
providedKeys: Object.keys(query),
expectedKeys: ['code', 'state'],
});
@ -198,14 +206,14 @@ export class SlackHandler {
* GET /api/v2/slack/integration
* Get current integration status
*/
async getIntegration(c: Context) {
async getIntegration(c: Context): Promise<Response> {
try {
// Get service instance (lazy initialization)
const slackOAuthService = this.getSlackOAuthService();
// Check if service is available
if (!slackOAuthService) {
return c.json(
return c.json<SlackErrorResponse>(
{
error: 'Slack integration is not configured',
code: 'INTEGRATION_NOT_CONFIGURED',
@ -228,7 +236,19 @@ export class SlackHandler {
const status = await slackOAuthService.getIntegrationStatus(organizationGrant.organizationId);
return c.json(status);
return c.json<GetIntegrationResponse>({
connected: status.connected,
integration: status.integration
? {
id: status.integration.id,
team_name: status.integration.teamName,
installed_at: status.integration.installedAt,
team_domain: status.integration.teamDomain,
last_used_at: status.integration.lastUsedAt,
default_channel: status.integration.defaultChannel,
}
: undefined,
});
} catch (error) {
console.error('Failed to get integration status:', error);
@ -248,14 +268,14 @@ export class SlackHandler {
* DELETE /api/v2/slack/integration
* Remove Slack integration
*/
async removeIntegration(c: Context) {
async removeIntegration(c: Context): Promise<Response> {
try {
// Get service instance (lazy initialization)
const slackOAuthService = this.getSlackOAuthService();
// Check if service is available
if (!slackOAuthService) {
return c.json(
return c.json<SlackErrorResponse>(
{
error: 'Slack integration is not configured',
code: 'INTEGRATION_NOT_CONFIGURED',
@ -289,7 +309,7 @@ export class SlackHandler {
);
}
return c.json({
return c.json<RemoveIntegrationResponse>({
message: 'Slack integration removed successfully',
});
} catch (error) {
@ -311,14 +331,14 @@ export class SlackHandler {
* PUT /api/v2/slack/integration
* Update Slack integration settings
*/
async updateIntegration(c: Context) {
async updateIntegration(c: Context): Promise<Response> {
try {
// Get service instance (lazy initialization)
const slackOAuthService = this.getSlackOAuthService();
// Check if service is available
if (!slackOAuthService) {
return c.json(
return c.json<SlackErrorResponse>(
{
error: 'Slack integration is not configured',
code: 'INTEGRATION_NOT_CONFIGURED',
@ -340,12 +360,12 @@ export class SlackHandler {
}
// Parse request body
const body = await c.req.json().catch(() => ({}));
const body: unknown = await c.req.json().catch(() => ({}));
const parsed = UpdateIntegrationSchema.safeParse(body);
if (!parsed.success) {
throw new SlackError(
`Invalid request body: ${parsed.error.errors.map((e) => e.message).join(', ')}`,
`Invalid request body: ${parsed.error.issues.map((e) => e.message).join(', ')}`,
400,
'INVALID_REQUEST_BODY'
);
@ -363,7 +383,7 @@ export class SlackHandler {
await updateDefaultChannel(integration.id, parsed.data.default_channel);
}
return c.json({
return c.json<UpdateIntegrationResponse>({
message: 'Integration updated successfully',
...parsed.data,
});
@ -386,14 +406,14 @@ export class SlackHandler {
* GET /api/v2/slack/channels
* Get public channels for the current integration
*/
async getChannels(c: Context) {
async getChannels(c: Context): Promise<Response> {
try {
// Get service instance (lazy initialization)
const slackOAuthService = this.getSlackOAuthService();
// Check if service is available
if (!slackOAuthService) {
return c.json(
return c.json<SlackErrorResponse>(
{
error: 'Slack integration is not configured',
code: 'INTEGRATION_NOT_CONFIGURED',
@ -418,7 +438,7 @@ export class SlackHandler {
const integration = await slackHelpers.getActiveIntegration(organizationGrant.organizationId);
if (!integration) {
return c.json(
return c.json<SlackErrorResponse>(
{
error: 'No active Slack integration found',
code: 'INTEGRATION_NOT_FOUND',
@ -446,7 +466,7 @@ export class SlackHandler {
await slackHelpers.updateLastUsedAt(integration.id);
// Return only id and name as requested
return c.json({
return c.json<GetChannelsResponse>({
channels: channels.map((channel) => ({
id: channel.id,
name: channel.name,

View File

@ -40,7 +40,13 @@ export class SlackOAuthService {
clientId: this.env.SLACK_CLIENT_ID,
clientSecret: this.env.SLACK_CLIENT_SECRET,
redirectUri: this.env.SLACK_REDIRECT_URI,
scopes: ['channels:history', 'chat:write', 'chat:write.public', 'commands'],
scopes: [
'channels:history',
'channels:read',
'chat:write',
'chat:write.public',
'commands',
],
},
tokenStorage,
oauthStateStorage

View File

@ -320,7 +320,13 @@ describe('data-transformers', () => {
});
it('should handle empty conversation history', () => {
const result = buildWorkflowInput(baseMessageContext, [], basePreviousResults, baseDatasets, false);
const result = buildWorkflowInput(
baseMessageContext,
[],
basePreviousResults,
baseDatasets,
false
);
expect(result.conversationHistory).toBeUndefined();
});
});

View File

@ -127,7 +127,7 @@ export async function getExistingSlackMessageForChat(chatId: string): Promise<{
* Send a Slack notification based on post-processing results
*/
export async function sendSlackNotification(
params: SlackNotificationParams,
params: SlackNotificationParams
): Promise<SlackNotificationResult> {
try {
// Step 1: Check if organization has active Slack integration
@ -139,8 +139,8 @@ export async function sendSlackNotification(
and(
eq(slackIntegrations.organizationId, params.organizationId),
eq(slackIntegrations.status, 'active'),
isNull(slackIntegrations.deletedAt),
),
isNull(slackIntegrations.deletedAt)
)
)
.limit(1);
@ -189,7 +189,7 @@ export async function sendSlackNotification(
const result = await sendSlackMessage(
tokenSecret.secret,
integration.defaultChannel.id,
slackMessage,
slackMessage
);
if (result.success) {
@ -229,7 +229,7 @@ export async function sendSlackNotification(
* Send a Slack reply notification to an existing thread
*/
export async function sendSlackReplyNotification(
params: SlackReplyNotificationParams,
params: SlackReplyNotificationParams
): Promise<SlackNotificationResult> {
try {
// Step 1: Check if we should send a notification
@ -257,7 +257,7 @@ export async function sendSlackReplyNotification(
tokenSecret.secret,
params.channelId,
slackMessage,
params.threadTs,
params.threadTs
);
if (result.success) {
@ -372,7 +372,7 @@ function formatSlackReplyMessage(params: SlackReplyNotificationParams): SlackMes
throw new Error(
'Invalid reply notification parameters: Missing required fields. ' +
'Requires either formattedMessage, summaryTitle with summaryMessage, or toolCalled="flagChat" with message',
'Requires either formattedMessage, summaryTitle with summaryMessage, or toolCalled="flagChat" with message'
);
}
@ -456,7 +456,7 @@ function formatSlackMessage(params: SlackNotificationParams): SlackMessage {
throw new Error(
`Invalid notification parameters: Missing required fields. Requires either formattedMessage, summaryTitle with summaryMessage, or toolCalled="flagChat" with message. Received: formattedMessage=${!!params.formattedMessage}, summaryTitle=${!!params.summaryTitle}, summaryMessage=${!!params.summaryMessage}, toolCalled="${
params.toolCalled
}", message=${!!params.message}`,
}", message=${!!params.message}`
);
}
@ -467,7 +467,7 @@ async function sendSlackMessage(
accessToken: string,
channelId: string,
message: SlackMessage,
threadTs?: string,
threadTs?: string
): Promise<{ success: boolean; messageTs?: string; error?: string }> {
try {
const response = await fetch('https://slack.com/api/chat.postMessage', {
@ -540,8 +540,8 @@ export async function trackSlackNotification(params: {
content: params.slackBlocks
? JSON.stringify({ blocks: params.slackBlocks })
: params.summaryTitle && params.summaryMessage
? `${params.summaryTitle}\n\n${params.summaryMessage}`
: 'Notification sent',
? `${params.summaryTitle}\n\n${params.summaryMessage}`
: 'Notification sent',
senderInfo: {
sentBy: 'buster-post-processing',
userName: params.userName,

View File

@ -24,14 +24,16 @@ vi.mock('@buster/database', () => ({
getDb: vi.fn(),
eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
messages: { id: 'messages.id' },
getBraintrustMetadata: vi.fn(() => Promise.resolve({
userName: 'John Doe',
userId: 'user-123',
organizationName: 'Test Org',
organizationId: 'org-123',
messageId: 'msg-12345',
chatId: 'chat-123',
})),
getBraintrustMetadata: vi.fn(() =>
Promise.resolve({
userName: 'John Doe',
userId: 'user-123',
organizationName: 'Test Org',
organizationId: 'org-123',
messageId: 'msg-12345',
chatId: 'chat-123',
})
),
}));
vi.mock('@buster/ai/workflows/post-processing-workflow', () => ({

View File

@ -19,7 +19,9 @@ export const combineParallelResultsOutputSchema = z.object({
userId: z.string().describe('User ID for the current operation'),
chatId: z.string().describe('Chat ID for the current operation'),
isFollowUp: z.boolean().describe('Whether this is a follow-up message'),
isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'),
isSlackFollowUp: z
.boolean()
.describe('Whether this is a follow-up message for an existing Slack thread'),
previousMessages: z.array(z.string()).describe('Array of previous messages for context'),
datasets: z.string().describe('Assembled YAML content of all available datasets for context'),

View File

@ -15,7 +15,9 @@ const inputSchema = z.object({
userId: z.string().describe('User ID for the current operation'),
chatId: z.string().describe('Chat ID for the current operation'),
isFollowUp: z.boolean().describe('Whether this is a follow-up message'),
isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'),
isSlackFollowUp: z
.boolean()
.describe('Whether this is a follow-up message for an existing Slack thread'),
previousMessages: z.array(z.string()).describe('Array of previous messages for context'),
datasets: z.string().describe('Assembled YAML content of all available datasets for context'),
});
@ -28,7 +30,9 @@ export const flagChatOutputSchema = z.object({
userId: z.string().describe('User ID for the current operation'),
chatId: z.string().describe('Chat ID for the current operation'),
isFollowUp: z.boolean().describe('Whether this is a follow-up message'),
isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'),
isSlackFollowUp: z
.boolean()
.describe('Whether this is a follow-up message for an existing Slack thread'),
previousMessages: z.array(z.string()).describe('Array of previous messages for context'),
datasets: z.string().describe('Assembled YAML content of all available datasets for context'),

View File

@ -18,7 +18,9 @@ const inputSchema = z.object({
userId: z.string().describe('User ID for the current operation'),
chatId: z.string().describe('Chat ID for the current operation'),
isFollowUp: z.boolean().describe('Whether this is a follow-up message'),
isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'),
isSlackFollowUp: z
.boolean()
.describe('Whether this is a follow-up message for an existing Slack thread'),
previousMessages: z.array(z.string()).describe('Array of previous messages for context'),
datasets: z.string().describe('Assembled YAML content of all available datasets for context'),
});
@ -31,7 +33,9 @@ export const identifyAssumptionsOutputSchema = z.object({
userId: z.string().describe('User ID for the current operation'),
chatId: z.string().describe('Chat ID for the current operation'),
isFollowUp: z.boolean().describe('Whether this is a follow-up message'),
isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'),
isSlackFollowUp: z
.boolean()
.describe('Whether this is a follow-up message for an existing Slack thread'),
previousMessages: z.array(z.string()).describe('Array of previous messages for context'),
datasets: z.string().describe('Assembled YAML content of all available datasets for context'),

View File

@ -9,7 +9,9 @@ export const postProcessingWorkflowInputSchema = z.object({
userId: z.string().describe('User ID for the current operation'),
chatId: z.string().describe('Chat ID for the current operation'),
isFollowUp: z.boolean().describe('Whether this is a follow-up message'),
isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'),
isSlackFollowUp: z
.boolean()
.describe('Whether this is a follow-up message for an existing Slack thread'),
previousMessages: z.array(z.string()).describe('Array of the previous post-processing messages'),
datasets: z.string().describe('Assembled YAML content of all available datasets for context'),
});
@ -23,7 +25,9 @@ export const postProcessingWorkflowOutputSchema = z.object({
userId: z.string().describe('User ID for the current operation'),
chatId: z.string().describe('Chat ID for the current operation'),
isFollowUp: z.boolean().describe('Whether this is a follow-up message'),
isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'),
isSlackFollowUp: z
.boolean()
.describe('Whether this is a follow-up message for an existing Slack thread'),
previousMessages: z.array(z.string()).describe('Array of the previous post-processing messages'),
datasets: z.string().describe('Assembled YAML content of all available datasets for context'),

View File

@ -30,7 +30,8 @@ export function validateAndAdjustBarLineAxes(metricYml: MetricYml): AxisValidati
return {
isValid: false,
shouldSwapAxes: false,
error: 'Bar and line charts require at least one column for each axis. Please specify both X and Y axis columns.',
error:
'Bar and line charts require at least one column for each axis. Please specify both X and Y axis columns.',
};
}
const xColumns = barAndLineAxis.x;