From 76b18c8765d085e1719db1f01aa4b6fcdf47e3f0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Jul 2025 20:02:29 +0000 Subject: [PATCH] Convert Slack response types to Zod schemas for runtime validation Co-authored-by: dallin --- .../v2/slack/services/slack-oauth-service.ts | 23 +++ packages/server-shared/src/slack/index.ts | 13 +- .../src/slack/responses.types.ts | 152 ++++++++++++------ 3 files changed, 126 insertions(+), 62 deletions(-) diff --git a/apps/server/src/api/v2/slack/services/slack-oauth-service.ts b/apps/server/src/api/v2/slack/services/slack-oauth-service.ts index cdfe85fa4..060e401e3 100644 --- a/apps/server/src/api/v2/slack/services/slack-oauth-service.ts +++ b/apps/server/src/api/v2/slack/services/slack-oauth-service.ts @@ -210,6 +210,10 @@ export class SlackOAuthService { teamDomain?: string; installedAt: string; lastUsedAt?: string; + defaultChannel?: { + id: string; + name: string; + }; }; }> { try { @@ -219,6 +223,19 @@ export class SlackOAuthService { return { connected: false }; } + // Cast defaultChannel to the expected type + const defaultChannel = integration.defaultChannel as + | { id: string; name: string } + | Record + | null; + + // Check if defaultChannel has content + const hasDefaultChannel = + defaultChannel && + typeof defaultChannel === 'object' && + 'id' in defaultChannel && + 'name' in defaultChannel; + return { connected: true, integration: { @@ -227,6 +244,12 @@ export class SlackOAuthService { ...(integration.teamDomain != null && { teamDomain: integration.teamDomain }), installedAt: integration.installedAt || integration.createdAt, ...(integration.lastUsedAt != null && { lastUsedAt: integration.lastUsedAt }), + ...(hasDefaultChannel && { + defaultChannel: { + id: defaultChannel.id, + name: defaultChannel.name, + }, + }), }, }; } catch (error) { diff --git a/packages/server-shared/src/slack/index.ts b/packages/server-shared/src/slack/index.ts index 7621d93fd..5758044b4 100644 --- a/packages/server-shared/src/slack/index.ts +++ b/packages/server-shared/src/slack/index.ts @@ -1,11 +1,4 @@ -// Export all request types and schemas -export * from './requests.types'; -export type * from './requests.types'; - -// Export all response types -export * from './responses.types'; -export type * from './responses.types'; - -// Export error types and classes +// Re-export all types, schemas, and utilities export * from './errors.types'; -export type * from './errors.types'; +export * from './requests.types'; +export * from './responses.types'; diff --git a/packages/server-shared/src/slack/responses.types.ts b/packages/server-shared/src/slack/responses.types.ts index 10d189d63..095c92916 100644 --- a/packages/server-shared/src/slack/responses.types.ts +++ b/packages/server-shared/src/slack/responses.types.ts @@ -1,69 +1,117 @@ -// Error response type -export interface SlackErrorResponse { - error: string; - code?: string; -} +import { z } from 'zod'; + +// Error response schema +export const SlackErrorResponseSchema = z.object({ + error: z.string(), + code: z.string().optional(), +}); + +export type SlackErrorResponse = z.infer; // POST /api/v2/slack/auth/init -export interface InitiateOAuthResponse { - auth_url: string; - state: string; -} +export const InitiateOAuthResponseSchema = z.object({ + auth_url: z.string(), + state: z.string(), +}); + +export type InitiateOAuthResponse = z.infer; // GET /api/v2/slack/auth/callback // This endpoint returns a redirect, not JSON // GET /api/v2/slack/integration -export interface GetIntegrationResponse { - connected: boolean; - integration?: { - id: string; - team_name: string; - team_domain?: string; - installed_at: string; - last_used_at?: string; - }; -} +export const GetIntegrationResponseSchema = z.object({ + connected: z.boolean(), + integration: z + .object({ + id: z.string(), + team_name: z.string(), + team_domain: z.string().optional(), + installed_at: z.string(), + last_used_at: z.string().optional(), + default_channel: z + .object({ + id: z.string(), + name: z.string(), + }) + .optional(), + }) + .optional(), +}); + +export type GetIntegrationResponse = z.infer; // DELETE /api/v2/slack/integration -export interface RemoveIntegrationResponse { - message: string; -} +export const RemoveIntegrationResponseSchema = z.object({ + message: z.string(), +}); + +export type RemoveIntegrationResponse = z.infer; // PUT /api/v2/slack/integration -export interface UpdateIntegrationResponse { - message: string; - default_channel?: { - name: string; - id: string; - }; -} +export const UpdateIntegrationResponseSchema = z.object({ + message: z.string(), + default_channel: z + .object({ + name: z.string(), + id: z.string(), + }) + .optional(), +}); + +export type UpdateIntegrationResponse = z.infer; // GET /api/v2/slack/channels -export interface GetChannelsResponse { - channels: Array<{ - id: string; - name: string; - }>; -} +export const GetChannelsResponseSchema = z.object({ + channels: z.array( + z.object({ + id: z.string(), + name: z.string(), + }) + ), +}); + +export type GetChannelsResponse = z.infer; // OAuth callback result (used internally) -export interface OAuthCallbackResult { - success: boolean; - integration_id: string; - metadata?: { - return_url?: string; - source?: string; - project_id?: string; - initiated_at?: string; - ip_address?: string; - }; - team_name?: string; - error?: string; -} +export const OAuthCallbackResultSchema = z.object({ + success: z.boolean(), + integration_id: z.string(), + metadata: z + .object({ + return_url: z.string().optional(), + source: z.string().optional(), + project_id: z.string().optional(), + initiated_at: z.string().optional(), + ip_address: z.string().optional(), + }) + .optional(), + team_name: z.string().optional(), + error: z.string().optional(), +}); + +export type OAuthCallbackResult = z.infer; // Remove integration result (used internally) -export interface RemoveIntegrationResult { - success: boolean; - error?: string; -} +export const RemoveIntegrationResultSchema = z.object({ + success: z.boolean(), + error: z.string().optional(), +}); + +export type RemoveIntegrationResult = z.infer; + +// Example usage for type validation: +// const response: GetIntegrationResponse = GetIntegrationResponseSchema.parse({ +// connected: true, +// integration: { +// id: 'integration-123', +// team_name: 'My Team', +// team_domain: 'my-team', +// installed_at: '2025-01-01T00:00:00Z', +// last_used_at: '2025-01-02T00:00:00Z', +// default_channel: { +// id: 'C04RCNXL75J', +// name: 'general' +// } +// } +// });