block-validators.ts•4.79 kB
import { z } from 'zod';
import type {
Block,
SectionBlock,
DividerBlock,
HeaderBlock,
ImageBlock,
ContextBlock,
ActionsBlock,
InputBlock,
RichTextBlock,
VideoBlock,
FileBlock,
} from '@slack/types';
// Text composition object schemas
const plainTextSchema = z.object({
type: z.literal('plain_text'),
text: z.string().max(3000),
emoji: z.boolean().optional(),
});
const mrkdwnTextSchema = z.object({
type: z.literal('mrkdwn'),
text: z.string().max(3000),
verbatim: z.boolean().optional(),
});
const textObjectSchema = z.union([plainTextSchema, mrkdwnTextSchema]);
// Common block elements (simplified validation for complex elements)
const elementSchema = z.object({
type: z.string(),
}).passthrough();
// Strict validation for common blocks
export const sectionBlockSchema = z.object({
type: z.literal('section'),
text: textObjectSchema.optional(),
block_id: z.string().max(255).optional(),
fields: z.array(textObjectSchema).max(10).optional(),
accessory: z.any().optional(),
}) as z.ZodType<SectionBlock>;
export const dividerBlockSchema = z.object({
type: z.literal('divider'),
block_id: z.string().max(255).optional(),
}) satisfies z.ZodType<DividerBlock>;
export const headerBlockSchema = z.object({
type: z.literal('header'),
text: plainTextSchema,
block_id: z.string().max(255).optional(),
}) satisfies z.ZodType<HeaderBlock>;
export const imageBlockSchema = z.object({
type: z.literal('image'),
image_url: z.string().url().max(3000),
alt_text: z.string().max(2000),
title: plainTextSchema.optional(),
block_id: z.string().max(255).optional(),
}) satisfies z.ZodType<ImageBlock>;
export const contextBlockSchema = z.object({
type: z.literal('context'),
elements: z.array(z.any()).min(1).max(10),
block_id: z.string().max(255).optional(),
}) as z.ZodType<ContextBlock>;
// Loose validation for complex blocks
export const actionsBlockSchema = z.object({
type: z.literal('actions'),
elements: z.array(z.any()).min(1).max(25),
block_id: z.string().max(255).optional(),
}) as z.ZodType<ActionsBlock>;
export const inputBlockSchema = z.object({
type: z.literal('input'),
label: plainTextSchema,
element: z.any(),
dispatch_action: z.boolean().optional(),
block_id: z.string().max(255).optional(),
hint: plainTextSchema.optional(),
optional: z.boolean().optional(),
}) as z.ZodType<InputBlock>;
export const richTextBlockSchema = z.object({
type: z.literal('rich_text'),
elements: z.array(z.any()),
block_id: z.string().max(255).optional(),
}) as z.ZodType<RichTextBlock>;
export const videoBlockSchema = z.object({
type: z.literal('video'),
video_url: z.string().url().max(3000),
thumbnail_url: z.string().url().max(3000),
alt_text: z.string().max(2000),
title: plainTextSchema,
title_url: z.string().url().max(3000).optional(),
author_name: z.string().max(50).optional(),
provider_name: z.string().max(50).optional(),
provider_icon_url: z.string().url().max(3000).optional(),
description: plainTextSchema.optional(),
block_id: z.string().max(255).optional(),
}) satisfies z.ZodType<VideoBlock>;
export const fileBlockSchema = z.object({
type: z.literal('file'),
external_id: z.string(),
source: z.literal('remote'),
block_id: z.string().max(255).optional(),
}) satisfies z.ZodType<FileBlock>;
// Discriminated union for all block types - cast to satisfy discriminatedUnion requirements
const baseSectionBlock = z.object({
type: z.literal('section'),
text: textObjectSchema.optional(),
block_id: z.string().max(255).optional(),
fields: z.array(textObjectSchema).max(10).optional(),
accessory: z.any().optional(),
});
const baseContextBlock = z.object({
type: z.literal('context'),
elements: z.array(z.any()).min(1).max(10),
block_id: z.string().max(255).optional(),
});
const baseActionsBlock = z.object({
type: z.literal('actions'),
elements: z.array(z.any()).min(1).max(25),
block_id: z.string().max(255).optional(),
});
const baseInputBlock = z.object({
type: z.literal('input'),
label: plainTextSchema,
element: z.any(),
dispatch_action: z.boolean().optional(),
block_id: z.string().max(255).optional(),
hint: plainTextSchema.optional(),
optional: z.boolean().optional(),
});
const baseRichTextBlock = z.object({
type: z.literal('rich_text'),
elements: z.array(z.any()),
block_id: z.string().max(255).optional(),
});
export const blockSchema = z.discriminatedUnion('type', [
baseSectionBlock,
dividerBlockSchema,
headerBlockSchema,
imageBlockSchema,
baseContextBlock,
baseActionsBlock,
baseInputBlock,
baseRichTextBlock,
videoBlockSchema,
fileBlockSchema,
]);
// Array of blocks with max limit
export const blocksArraySchema = z.array(blockSchema).max(50) as z.ZodType<Block[]>;