Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/kit

src/broadcasts.ts

269 lines · 8.1 KB · TypeScript
import { kitListAll, kitRequest, unwrapRecord } from './client.ts'
import { mutationPreview, rejectBroadcastSend } from './safety.ts'
import { parseAuthInput } from './auth.ts'
import { broadcastSummary } from './models.ts'
import type { BroadcastSummary, JsonRecord, KitAuthInput, MutationInput } from './types.ts'
import {
	clampInt,
	compactRecord,
	optionalBoolean,
	optionalString,
	requireId,
	requireRecord,
	requireString,
} from './types.ts'

export type BroadcastDraftInput = KitAuthInput &
	MutationInput & {
		subject: string
		content: string
		preview_text?: string
		description?: string
		thumbnail_url?: string
		thumbnail_alt?: string
		email_address?: string
		email_template_id?: number
		public?: boolean
		subscriber_filter?: unknown
	}

export type BroadcastDetail = BroadcastSummary & {
	preview_text?: unknown
	description?: unknown
	content?: unknown
	thumbnail_url?: unknown
	thumbnail_alt?: unknown
	email_address?: unknown
	email_template?: unknown
	public?: unknown
	send_at?: unknown
	subscriber_filter?: unknown
	public_url?: unknown
}

function broadcastDetail(broadcast: JsonRecord): BroadcastDetail {
	const summary = broadcastSummary(broadcast)
	if (!summary) throw new Error('Kit returned no broadcast id.')
	return {
		...summary,
		preview_text: broadcast.preview_text,
		description: broadcast.description,
		content: broadcast.content,
		thumbnail_url: broadcast.thumbnail_url,
		thumbnail_alt: broadcast.thumbnail_alt,
		email_address: broadcast.email_address,
		email_template: broadcast.email_template,
		public: broadcast.public,
		send_at: broadcast.send_at,
		subscriber_filter: broadcast.subscriber_filter,
		public_url: broadcast.public_url,
	}
}

function draftBody(input: BroadcastDraftInput | Record<string, unknown>): JsonRecord {
	rejectBroadcastSend(input as JsonRecord, 'broadcast draft')
	return compactRecord({
		subject: input.subject,
		content: input.content,
		preview_text: input.preview_text,
		description: input.description,
		thumbnail_url: input.thumbnail_url,
		thumbnail_alt: input.thumbnail_alt,
		email_address: input.email_address,
		email_template_id: input.email_template_id,
		public: input.public,
		subscriber_filter: input.subscriber_filter,
	})
}

/**
 * Create a Kit broadcast **draft**. Never sends or schedules. Requires
 * `confirm: true`, or use `dryRun: true`. A human sends from the Kit UI via
 * `editUrl`.
 * @example
 * import { createBroadcastDraft } from 'kody:@kody/kit/broadcasts'
 * const preview = await createBroadcastDraft({
 *   subject: 'Hello',
 *   content: '<p>Hi</p>',
 *   dryRun: true,
 * })
 */
export async function createBroadcastDraft(input: BroadcastDraftInput) {
	const subject = requireString(input.subject, 'subject')
	const content = requireString(input.content, 'content')
	const body = draftBody({ ...input, subject, content })
	const preview = mutationPreview(input, {
		method: 'POST',
		path: '/broadcasts',
		body,
	})
	if (preview) return preview
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'POST',
		path: '/broadcasts',
		body,
		confirm: true,
	})
	return broadcastDetail(unwrapRecord(result.data, 'broadcast', 'createBroadcastDraft'))
}

/**
 * Update a broadcast draft. Refuses send_at / send statuses. Requires
 * `confirm: true`, or use `dryRun: true`.
 * @example
 * import { updateBroadcast } from 'kody:@kody/kit/broadcasts'
 * const preview = await updateBroadcast({ id: 1, subject: 'Updated', dryRun: true })
 */
export async function updateBroadcast(
	input: KitAuthInput & MutationInput & Partial<BroadcastDraftInput> & { id: number | string },
) {
	const id = requireId(input.id, 'id')
	const body = draftBody(input)
	const preview = mutationPreview(input, {
		method: 'PUT',
		path: `/broadcasts/${id}`,
		body,
	})
	if (preview) return preview
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'PUT',
		path: `/broadcasts/${id}`,
		body,
		confirm: true,
	})
	return broadcastDetail(unwrapRecord(result.data, 'broadcast', 'updateBroadcast'))
}

/**
 * Get one broadcast.
 * @example
 * import { getBroadcast } from 'kody:@kody/kit/broadcasts'
 * const draft = await getBroadcast({ id: 123 })
 */
export async function getBroadcast(input: KitAuthInput & { id: number | string }) {
	const id = requireId(input.id, 'id')
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'GET',
		path: `/broadcasts/${id}`,
	})
	return broadcastDetail(unwrapRecord(result.data, 'broadcast', 'getBroadcast'))
}

/**
 * List broadcast summaries. Always pass `maxItems` for exploratory reads
 * (default 25). This package never sends broadcasts.
 * @example
 * import { listBroadcasts } from 'kody:@kody/kit/broadcasts'
 * const recent = await listBroadcasts({ maxItems: 10 })
 */
export async function listBroadcasts(
	input: KitAuthInput & { status?: string; maxItems?: number } = {},
): Promise<Array<BroadcastSummary>> {
	const items = await kitListAll<JsonRecord>({
		...input,
		path: '/broadcasts',
		itemKey: 'broadcasts',
		maxItems: clampInt(input.maxItems, 1, 500, 25),
		query: compactRecord({ status: input.status }),
	})
	return items
		.map((item) => broadcastSummary(item))
		.filter((item): item is BroadcastSummary => Boolean(item))
}

/**
 * Delete a broadcast draft. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { deleteBroadcast } from 'kody:@kody/kit/broadcasts'
 * const preview = await deleteBroadcast({ id: 123, dryRun: true })
 */
export async function deleteBroadcast(input: KitAuthInput & MutationInput & { id: number | string }) {
	const id = requireId(input.id, 'id')
	const preview = mutationPreview(input, {
		method: 'DELETE',
		path: `/broadcasts/${id}`,
	})
	if (preview) return preview
	await kitRequest({
		...input,
		method: 'DELETE',
		path: `/broadcasts/${id}`,
		confirm: true,
	})
	return { id, deleted: true as const }
}

/**
 * Read stats for one broadcast.
 * @example
 * import { getBroadcastStats } from 'kody:@kody/kit/broadcasts'
 * const stats = await getBroadcastStats({ id: 123 })
 */
export async function getBroadcastStats(input: KitAuthInput & { id: number | string }) {
	const id = requireId(input.id, 'id')
	const result = await kitRequest({
		...input,
		method: 'GET',
		path: `/broadcasts/${id}/stats`,
	})
	return result.data
}

/**
 * Broadcast helpers. Defaults to `list`. Drafts only — never sends.
 * @example
 * import broadcasts from 'kody:@kody/kit/broadcasts'
 * const recent = await broadcasts({ action: 'list', maxItems: 5 })
 */
export default async function broadcastsEntrypoint(params: Record<string, unknown> = {}) {
	const input = requireRecord(params, 'broadcasts')
	const auth = parseAuthInput(input)
	const action = optionalString(input.action, 'action') ?? 'list'
	const mutation = {
		confirm: optionalBoolean(input.confirm, 'confirm'),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
	}
	switch (action) {
		case 'list':
			return listBroadcasts({
				...auth,
				status: optionalString(input.status, 'status'),
				maxItems: input.maxItems as number | undefined,
			})
		case 'get':
			return getBroadcast({ ...auth, id: requireId(input.id, 'id') })
		case 'stats':
			return getBroadcastStats({ ...auth, id: requireId(input.id, 'id') })
		case 'create-draft':
			return createBroadcastDraft({
				...auth,
				...mutation,
				subject: requireString(input.subject, 'subject'),
				content: requireString(input.content, 'content'),
				preview_text: optionalString(input.preview_text, 'preview_text'),
				description: optionalString(input.description, 'description'),
				email_address: optionalString(input.email_address, 'email_address'),
				email_template_id: input.email_template_id as number | undefined,
				public: optionalBoolean(input.public, 'public'),
			})
		case 'update':
			return updateBroadcast({
				...auth,
				...mutation,
				id: requireId(input.id, 'id'),
				subject: optionalString(input.subject, 'subject'),
				content: optionalString(input.content, 'content'),
			})
		case 'delete':
			return deleteBroadcast({ ...auth, ...mutation, id: requireId(input.id, 'id') })
		default:
			throw new Error(
				'broadcasts action must be one of: list, get, stats, create-draft, update, delete.',
			)
	}
}

export { broadcastSummary }