Skip to content

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

Package listing

@kody/kit

src/subscribers.ts

310 lines · 9.3 KB · TypeScript
import { kitListAll, kitRequest, unwrapRecord } from './client.ts'
import { mutationPreview } from './safety.ts'
import { parseAuthInput } from './auth.ts'
import { subscriberSummary } from './models.ts'
import { ensureTag, tagSubscriber } from './tags.ts'
import type { DryRunResult, JsonRecord, KitAuthInput, MutationInput, SubscriberSummary } from './types.ts'
import {
	clampInt,
	compactRecord,
	optionalBoolean,
	optionalString,
	requireId,
	requireRecord,
	requireString,
} from './types.ts'

export type SubscriberListInput = KitAuthInput & {
	email_address?: string
	status?: string
	created_after?: string
	sort_order?: string
	maxItems?: number
}

export type CreateSubscriberInput = KitAuthInput &
	MutationInput & {
		email_address: string
		first_name?: string
		state?: string
		fields?: Record<string, string>
	}

export type SubscribeAndTagInput = CreateSubscriberInput & {
	tagName?: string
	tagId?: number | string
}

export { subscriberSummary }

function requireSubscriber(body: unknown, action: string): SubscriberSummary {
	const subscriber = unwrapRecord(body, 'subscriber', action)
	const summary = subscriberSummary(subscriber)
	if (!summary) throw new Error(`${action}: Kit returned no subscriber id.`)
	return summary
}

/**
 * List subscribers. Pass `maxItems` (default 25) for exploratory reads.
 * @example
 * import { listSubscribers } from 'kody:@kody/kit/subscribers'
 * const people = await listSubscribers({ maxItems: 10 })
 */
export async function listSubscribers(input: SubscriberListInput = {}): Promise<Array<SubscriberSummary>> {
	const maxItems = clampInt(input.maxItems, 1, 500, 25)
	const items = await kitListAll<JsonRecord>({
		...input,
		path: '/subscribers',
		itemKey: 'subscribers',
		maxItems,
		query: compactRecord({
			email_address: input.email_address,
			status: input.status,
			created_after: input.created_after,
			sort_order: input.sort_order,
		}),
	})
	return items.map((item) => subscriberSummary(item)).filter((item): item is SubscriberSummary => Boolean(item))
}

/**
 * Get one subscriber by id.
 * @example
 * import { getSubscriber } from 'kody:@kody/kit/subscribers'
 * const person = await getSubscriber({ id: 123 })
 */
export async function getSubscriber(input: KitAuthInput & { id: number | string }) {
	const id = requireId(input.id, 'id')
	const result = await kitRequest({
		...input,
		method: 'GET',
		path: `/subscribers/${id}`,
	})
	return requireSubscriber(result.data, 'getSubscriber')
}

/**
 * Look up one subscriber by exact email. Returns null when not found.
 * @example
 * import { getSubscriberByEmail } from 'kody:@kody/kit/subscribers'
 * const person = await getSubscriberByEmail({ email_address: 'ada@example.com' })
 */
export async function getSubscriberByEmail(input: KitAuthInput & { email_address: string }) {
	const email = requireString(input.email_address, 'email_address')
	const result = await kitRequest<JsonRecord>({
		...input,
		method: 'GET',
		path: '/subscribers',
		query: { email_address: email },
	})
	const subscribers = Array.isArray(result.data.subscribers)
		? (result.data.subscribers as Array<JsonRecord>)
		: []
	return subscriberSummary(subscribers[0])
}

/**
 * Create or upsert a subscriber. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { createSubscriber } from 'kody:@kody/kit/subscribers'
 * const preview = await createSubscriber({ email_address: 'ada@example.com', dryRun: true })
 */
export async function createSubscriber(input: CreateSubscriberInput) {
	const email = requireString(input.email_address, 'email_address')
	const body = compactRecord({
		email_address: email,
		first_name: input.first_name,
		state: input.state,
		fields: input.fields,
	})
	const preview = mutationPreview(input, {
		method: 'POST',
		path: '/subscribers',
		body,
	})
	if (preview) return preview
	const result = await kitRequest({
		...input,
		method: 'POST',
		path: '/subscribers',
		body,
		confirm: true,
	})
	return requireSubscriber(result.data, 'createSubscriber')
}

/**
 * Update a subscriber. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { updateSubscriber } from 'kody:@kody/kit/subscribers'
 * const preview = await updateSubscriber({ id: 123, first_name: 'Ada', dryRun: true })
 */
export async function updateSubscriber(
	input: KitAuthInput & MutationInput & { id: number | string; first_name?: string; fields?: Record<string, string> },
) {
	const id = requireId(input.id, 'id')
	const body = compactRecord({
		first_name: input.first_name,
		fields: input.fields,
	})
	const preview = mutationPreview(input, {
		method: 'PUT',
		path: `/subscribers/${id}`,
		body,
	})
	if (preview) return preview
	const result = await kitRequest({
		...input,
		method: 'PUT',
		path: `/subscribers/${id}`,
		body,
		confirm: true,
	})
	return requireSubscriber(result.data, 'updateSubscriber')
}

/**
 * Unsubscribe a subscriber. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { unsubscribeSubscriber } from 'kody:@kody/kit/subscribers'
 * const preview = await unsubscribeSubscriber({ id: 123, dryRun: true })
 */
export async function unsubscribeSubscriber(input: KitAuthInput & MutationInput & { id: number | string }) {
	const id = requireId(input.id, 'id')
	const preview = mutationPreview(input, {
		method: 'POST',
		path: `/subscribers/${id}/unsubscribe`,
		body: {},
	})
	if (preview) return preview
	await kitRequest({
		...input,
		method: 'POST',
		path: `/subscribers/${id}/unsubscribe`,
		body: {},
		confirm: true,
	})
	return { id, unsubscribed: true as const }
}

/**
 * List tags on one subscriber.
 * @example
 * import { listSubscriberTags } from 'kody:@kody/kit/subscribers'
 * const tags = await listSubscriberTags({ id: 123 })
 */
export async function listSubscriberTags(input: KitAuthInput & { id: number | string; maxItems?: number }) {
	const id = requireId(input.id, 'id')
	return kitListAll({
		...input,
		path: `/subscribers/${id}/tags`,
		itemKey: 'tags',
		maxItems: clampInt(input.maxItems, 1, 500, 100),
	})
}

/**
 * Upsert a subscriber and apply a tag (created on first use when `tagName` is given).
 * Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { subscribeAndTag } from 'kody:@kody/kit/subscribers'
 * const preview = await subscribeAndTag({
 *   email_address: 'ada@example.com',
 *   tagName: 'waitlist',
 *   dryRun: true,
 * })
 */
export async function subscribeAndTag(input: SubscribeAndTagInput) {
	const email = requireString(input.email_address, 'email_address')
	if (!input.tagName && input.tagId === undefined) {
		throw new Error('subscribeAndTag: tagName or tagId is required.')
	}
	const preview = mutationPreview(input, {
		method: 'POST',
		path: '/subscribers+tag',
		body: compactRecord({
			email_address: email,
			first_name: input.first_name,
			fields: input.fields,
			tagName: input.tagName,
			tagId: input.tagId,
		}),
	})
	if (preview) return preview
	const subscriber = await createSubscriber({ ...input, confirm: true })
	if ('dryRun' in subscriber) return subscriber
	const tag = input.tagId
		? { id: Number(input.tagId), name: input.tagName }
		: await ensureTag({ ...input, name: requireString(input.tagName, 'tagName'), confirm: true })
	if ('dryRun' in tag) return tag
	const tagged = await tagSubscriber({
		...input,
		tagId: tag.id,
		email_address: email,
		confirm: true,
	})
	if ('dryRun' in tagged) return tagged
	return { subscriber, tag }
}

/**
 * Subscriber helpers. Defaults to `list`.
 * @example
 * import subscribers from 'kody:@kody/kit/subscribers'
 * const people = await subscribers({ action: 'list', maxItems: 10 })
 */
export default async function subscribersEntrypoint(
	params: Record<string, unknown> = {},
) {
	const input = requireRecord(params, 'subscribers')
	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 listSubscribers({ ...auth, ...input, maxItems: input.maxItems as number | undefined })
		case 'get':
			return getSubscriber({ ...auth, id: requireId(input.id, 'id') })
		case 'get-by-email':
			return getSubscriberByEmail({
				...auth,
				email_address: requireString(input.email_address, 'email_address'),
			})
		case 'create':
			return createSubscriber({
				...auth,
				...mutation,
				email_address: requireString(input.email_address, 'email_address'),
				first_name: optionalString(input.first_name, 'first_name'),
			})
		case 'update':
			return updateSubscriber({
				...auth,
				...mutation,
				id: requireId(input.id, 'id'),
				first_name: optionalString(input.first_name, 'first_name'),
			})
		case 'unsubscribe':
			return unsubscribeSubscriber({ ...auth, ...mutation, id: requireId(input.id, 'id') })
		case 'tags':
			return listSubscriberTags({ ...auth, id: requireId(input.id, 'id') })
		case 'subscribe-and-tag':
			return subscribeAndTag({
				...auth,
				...mutation,
				email_address: requireString(input.email_address, 'email_address'),
				first_name: optionalString(input.first_name, 'first_name'),
				tagName: optionalString(input.tagName, 'tagName'),
				tagId: input.tagId as number | string | undefined,
			})
		default:
			throw new Error(
				'subscribers action must be one of: list, get, get-by-email, create, update, unsubscribe, tags, subscribe-and-tag.',
			)
	}
}

export type { DryRunResult }