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 }