Skip to content

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

Package listing

@kody/resend

src/broadcasts.ts

208 lines · 6.0 KB · TypeScript
import {
	broadcastSummary,
	DASHBOARD_URL,
	mutationPreview,
	parseAction,
	resendRequest,
	unwrapList,
	type MutationGuardInput,
	type ResendAuthOptions,
} from './resend-core.ts'

export type CreateBroadcastDraftInput = MutationGuardInput & {
	from: string
	subject: string
	html?: string
	text?: string
	name?: string
	reply_to?: string | string[]
	preview_text?: string
	segment_id?: string
	audience_id?: string
	topic_id?: string
}

/**
 * POST /broadcasts — create a DRAFT. Nothing emails until sendBroadcast
 * (or a human sends it from the dashboard `editUrl`).
 */
export async function createBroadcastDraft(input: CreateBroadcastDraftInput) {
	if (!input.from) throw new Error('createBroadcastDraft: from is required')
	if (!input.subject) throw new Error('createBroadcastDraft: subject is required')
	if (!input.segment_id && !input.audience_id) {
		throw new Error('createBroadcastDraft: provide segment_id (or legacy audience_id)')
	}
	if (!input.html && !input.text) {
		throw new Error('createBroadcastDraft: provide html or text')
	}
	const body = {
		from: input.from,
		subject: input.subject,
		html: input.html,
		text: input.text,
		name: input.name,
		reply_to: input.reply_to,
		preview_text: input.preview_text,
		segment_id: input.segment_id,
		audience_id: input.audience_id,
		topic_id: input.topic_id,
	}
	const preview = mutationPreview(input, {
		action: 'create broadcast draft',
		method: 'POST',
		path: '/broadcasts',
		body,
	})
	if (preview) return preview
	const created = await resendRequest({
		path: '/broadcasts',
		method: 'POST',
		body,
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
	return created?.id ? { ...created, editUrl: DASHBOARD_URL + '/broadcasts/' + created.id } : created
}

/** GET /broadcasts */
export async function listBroadcasts(input: ResendAuthOptions = {}) {
	const body = await resendRequest({
		path: '/broadcasts',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
	return unwrapList(body).map((broadcast) => broadcastSummary(broadcast)).filter(Boolean)
}

/** GET /broadcasts/{id} */
export async function getBroadcast(input: ResendAuthOptions & { broadcastId: string }) {
	if (!input.broadcastId) throw new Error('getBroadcast: broadcastId is required')
	return await resendRequest({
		path: '/broadcasts/' + encodeURIComponent(input.broadcastId),
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

export type UpdateBroadcastInput = MutationGuardInput & {
	broadcastId: string
	from?: string
	subject?: string
	html?: string
	text?: string
	name?: string
	reply_to?: string | string[]
	preview_text?: string
	segment_id?: string
}

/** PATCH /broadcasts/{id} */
export async function updateBroadcast(input: UpdateBroadcastInput) {
	if (!input.broadcastId) throw new Error('updateBroadcast: broadcastId is required')
	const body = {
		from: input.from,
		subject: input.subject,
		html: input.html,
		text: input.text,
		name: input.name,
		reply_to: input.reply_to,
		preview_text: input.preview_text,
		segment_id: input.segment_id,
	}
	const preview = mutationPreview(input, {
		action: 'update broadcast ' + input.broadcastId,
		method: 'PATCH',
		path: '/broadcasts/' + input.broadcastId,
		body,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/broadcasts/' + encodeURIComponent(input.broadcastId),
		method: 'PATCH',
		body,
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/**
 * POST /broadcasts/{id}/send — emails the whole segment. Requires confirm.
 * Pass scheduledAt to schedule instead of sending immediately.
 */
export async function sendBroadcast(
	input: MutationGuardInput & { broadcastId: string; scheduledAt?: string },
) {
	if (!input.broadcastId) throw new Error('sendBroadcast: broadcastId is required')
	const body = input.scheduledAt ? { scheduled_at: input.scheduledAt } : {}
	const preview = mutationPreview(input, {
		action: 'send broadcast ' + input.broadcastId,
		method: 'POST',
		path: '/broadcasts/' + input.broadcastId + '/send',
		body,
		requireConfirm: true,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/broadcasts/' + encodeURIComponent(input.broadcastId) + '/send',
		method: 'POST',
		body,
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** DELETE /broadcasts/{id}. Destructive; requires confirm: true. */
export async function deleteBroadcast(input: MutationGuardInput & { broadcastId: string }) {
	if (!input.broadcastId) throw new Error('deleteBroadcast: broadcastId is required')
	const preview = mutationPreview(input, {
		action: 'delete broadcast ' + input.broadcastId,
		method: 'DELETE',
		path: '/broadcasts/' + input.broadcastId,
		requireConfirm: true,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/broadcasts/' + encodeURIComponent(input.broadcastId),
		method: 'DELETE',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

const broadcastActions = [
	'list-broadcasts',
	'get-broadcast',
	'create-broadcast-draft',
	'update-broadcast',
	'send-broadcast',
	'delete-broadcast',
] as const

/** Broadcasts dispatcher. Defaults to list-broadcasts. */
export default async function broadcasts(input: Record<string, unknown> = {}) {
	const action = parseAction(input.action, broadcastActions, 'list-broadcasts', 'broadcasts')
	switch (action) {
		case 'list-broadcasts':
			return await listBroadcasts(input)
		case 'get-broadcast':
			return await getBroadcast(input as never)
		case 'create-broadcast-draft':
			return await createBroadcastDraft(input as CreateBroadcastDraftInput)
		case 'update-broadcast':
			return await updateBroadcast(input as UpdateBroadcastInput)
		case 'send-broadcast':
			return await sendBroadcast(input as never)
		case 'delete-broadcast':
			return await deleteBroadcast(input as never)
		default: {
			const exhaustive: never = action
			throw new Error('Unhandled broadcasts action: ' + String(exhaustive))
		}
	}
}