Skip to content

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

Package listing

@kody/kit

src/sequences.ts

335 lines · 9.8 KB · TypeScript
import { kitListAll, kitRequest, unwrapRecord } from './client.ts'
import { mutationPreview } from './safety.ts'
import { parseAuthInput } from './auth.ts'
import { sequenceEmailSummary, sequenceSummary, subscriberSummary } from './models.ts'
import type { JsonRecord, KitAuthInput, MutationInput, SequenceSummary } from './types.ts'
import {
	clampInt,
	compactRecord,
	optionalBoolean,
	optionalString,
	requireId,
	requireRecord,
	requireString,
} from './types.ts'

export type SequenceCreateInput = KitAuthInput &
	MutationInput & {
		name: string
		email_address?: string
		email_template_id?: number
		active?: boolean
		repeat?: boolean
	}

export type SequenceEmailInput = KitAuthInput &
	MutationInput & {
		sequenceId: number | string
		subject: string
		content?: string
		published?: boolean
		preview_text?: string
		delay_value?: number
		delay_unit?: string
	}

/**
 * List sequences. Pass `maxItems` (default 25).
 * @example
 * import { listSequences } from 'kody:@kody/kit/sequences'
 * const sequences = await listSequences({ maxItems: 10 })
 */
export async function listSequences(
	input: KitAuthInput & { maxItems?: number } = {},
): Promise<Array<SequenceSummary>> {
	const items = await kitListAll<JsonRecord>({
		...input,
		path: '/sequences',
		itemKey: 'sequences',
		maxItems: clampInt(input.maxItems, 1, 500, 25),
	})
	return items
		.map((item) => sequenceSummary(item))
		.filter((item): item is SequenceSummary => Boolean(item))
}

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

/**
 * Create a sequence. Requires `confirm: true`, or use `dryRun: true`.
 * Activating a sequence can email subscribers — pass `active` only after
 * confirmation.
 * @example
 * import { createSequence } from 'kody:@kody/kit/sequences'
 * const preview = await createSequence({ name: 'Welcome', dryRun: true })
 */
export async function createSequence(input: SequenceCreateInput) {
	const name = requireString(input.name, 'name')
	const body = compactRecord({
		name,
		email_address: input.email_address,
		email_template_id: input.email_template_id,
		active: input.active,
		repeat: input.repeat,
	})
	const preview = mutationPreview(input, {
		method: 'POST',
		path: '/sequences',
		body,
	})
	if (preview) return preview
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'POST',
		path: '/sequences',
		body,
		confirm: true,
	})
	return unwrapRecord(result.data, 'sequence', 'createSequence')
}

/**
 * List emails in a sequence.
 * @example
 * import { listSequenceEmails } from 'kody:@kody/kit/sequences'
 * const emails = await listSequenceEmails({ sequenceId: 123 })
 */
export async function listSequenceEmails(
	input: KitAuthInput & { sequenceId: number | string; maxItems?: number },
) {
	const sequenceId = requireId(input.sequenceId, 'sequenceId')
	return kitListAll({
		...input,
		path: `/sequences/${sequenceId}/emails`,
		itemKey: 'emails',
		maxItems: clampInt(input.maxItems, 1, 500, 50),
	})
}

/**
 * Create a sequence email. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { createSequenceEmail } from 'kody:@kody/kit/sequences'
 * const preview = await createSequenceEmail({
 *   sequenceId: 1,
 *   subject: 'Thanks for joining',
 *   content: '<p>Welcome</p>',
 *   dryRun: true,
 * })
 */
export async function createSequenceEmail(input: SequenceEmailInput) {
	const sequenceId = requireId(input.sequenceId, 'sequenceId')
	const subject = requireString(input.subject, 'subject')
	const body = compactRecord({
		subject,
		content: input.content,
		published: input.published,
		preview_text: input.preview_text,
		delay_value: input.delay_value,
		delay_unit: input.delay_unit,
	})
	const preview = mutationPreview(input, {
		method: 'POST',
		path: `/sequences/${sequenceId}/emails`,
		body,
	})
	if (preview) return preview
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'POST',
		path: `/sequences/${sequenceId}/emails`,
		body,
		confirm: true,
	})
	const email = unwrapRecord(result.data, 'email', 'createSequenceEmail')
	return { ...sequenceEmailSummary(email), content: email.content }
}

/**
 * Get one sequence email.
 * @example
 * import { getSequenceEmail } from 'kody:@kody/kit/sequences'
 * const email = await getSequenceEmail({ sequenceId: 1, emailId: 2 })
 */
export async function getSequenceEmail(
	input: KitAuthInput & { sequenceId: number | string; emailId: number | string },
) {
	const sequenceId = requireId(input.sequenceId, 'sequenceId')
	const emailId = requireId(input.emailId, 'emailId')
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'GET',
		path: `/sequences/${sequenceId}/emails/${emailId}`,
	})
	const email = unwrapRecord(result.data, 'email', 'getSequenceEmail')
	return {
		...sequenceEmailSummary(email),
		preview_text: email.preview_text,
		content: email.content,
		delay_value: email.delay_value,
		delay_unit: email.delay_unit,
		position: email.position,
		email_template_id: email.email_template_id,
	}
}

/**
 * Update a sequence email. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { updateSequenceEmail } from 'kody:@kody/kit/sequences'
 * const preview = await updateSequenceEmail({
 *   sequenceId: 1,
 *   emailId: 2,
 *   subject: 'Updated',
 *   dryRun: true,
 * })
 */
export async function updateSequenceEmail(
	input: KitAuthInput &
		MutationInput & {
			sequenceId: number | string
			emailId: number | string
			subject?: string
			content?: string
			published?: boolean
		},
) {
	const sequenceId = requireId(input.sequenceId, 'sequenceId')
	const emailId = requireId(input.emailId, 'emailId')
	const body = compactRecord({
		subject: input.subject,
		content: input.content,
		published: input.published,
	})
	const preview = mutationPreview(input, {
		method: 'PUT',
		path: `/sequences/${sequenceId}/emails/${emailId}`,
		body,
	})
	if (preview) return preview
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'PUT',
		path: `/sequences/${sequenceId}/emails/${emailId}`,
		body,
		confirm: true,
	})
	const email = unwrapRecord(result.data, 'email', 'updateSequenceEmail')
	return { ...sequenceEmailSummary(email), content: email.content }
}

/**
 * Enroll a subscriber in a sequence by email. Requires `confirm: true`, or
 * use `dryRun: true`. An active sequence may email immediately.
 * @example
 * import { addSubscriberToSequence } from 'kody:@kody/kit/sequences'
 * const preview = await addSubscriberToSequence({
 *   sequenceId: 1,
 *   email_address: 'ada@example.com',
 *   dryRun: true,
 * })
 */
export async function addSubscriberToSequence(
	input: KitAuthInput & MutationInput & { sequenceId: number | string; email_address: string },
) {
	const sequenceId = requireId(input.sequenceId, 'sequenceId')
	const email = requireString(input.email_address, 'email_address')
	const preview = mutationPreview(input, {
		method: 'POST',
		path: `/sequences/${sequenceId}/subscribers`,
		body: { email_address: email },
	})
	if (preview) return preview
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'POST',
		path: `/sequences/${sequenceId}/subscribers`,
		body: { email_address: email },
		confirm: true,
	})
	return subscriberSummary(unwrapRecord(result.data, 'subscriber', 'addSubscriberToSequence'))
}

/**
 * Sequence helpers. Defaults to `list`.
 * @example
 * import sequences from 'kody:@kody/kit/sequences'
 * const all = await sequences({ action: 'list', maxItems: 10 })
 */
export default async function sequencesEntrypoint(params: Record<string, unknown> = {}) {
	const input = requireRecord(params, 'sequences')
	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 listSequences({ ...auth, maxItems: input.maxItems as number | undefined })
		case 'get':
			return getSequence({ ...auth, id: requireId(input.id, 'id') })
		case 'create':
			return createSequence({
				...auth,
				...mutation,
				name: requireString(input.name, 'name'),
				email_address: optionalString(input.email_address, 'email_address'),
				active: optionalBoolean(input.active, 'active'),
			})
		case 'emails':
			return listSequenceEmails({
				...auth,
				sequenceId: requireId(input.sequenceId ?? input.id, 'sequenceId'),
			})
		case 'create-email':
			return createSequenceEmail({
				...auth,
				...mutation,
				sequenceId: requireId(input.sequenceId, 'sequenceId'),
				subject: requireString(input.subject, 'subject'),
				content: optionalString(input.content, 'content'),
			})
		case 'get-email':
			return getSequenceEmail({
				...auth,
				sequenceId: requireId(input.sequenceId, 'sequenceId'),
				emailId: requireId(input.emailId, 'emailId'),
			})
		case 'update-email':
			return updateSequenceEmail({
				...auth,
				...mutation,
				sequenceId: requireId(input.sequenceId, 'sequenceId'),
				emailId: requireId(input.emailId, 'emailId'),
				subject: optionalString(input.subject, 'subject'),
				content: optionalString(input.content, 'content'),
				published: optionalBoolean(input.published, 'published'),
			})
		case 'add-subscriber':
			return addSubscriberToSequence({
				...auth,
				...mutation,
				sequenceId: requireId(input.sequenceId, 'sequenceId'),
				email_address: requireString(input.email_address, 'email_address'),
			})
		default:
			throw new Error(
				'sequences action must be one of: list, get, create, emails, create-email, get-email, update-email, add-subscriber.',
			)
	}
}