Skip to content

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

Package listing

@kody/kit

src/tags.ts

199 lines · 6.2 KB · TypeScript
import { kitListAll, kitRequest, unwrapRecord } from './client.ts'
import { mutationPreview } from './safety.ts'
import { parseAuthInput } from './auth.ts'
import { subscriberSummary, tagSummary } from './models.ts'
import type { JsonRecord, KitAuthInput, MutationInput, TagSummary } from './types.ts'
import {
	clampInt,
	compactRecord,
	optionalBoolean,
	optionalString,
	requireId,
	requireRecord,
	requireString,
} from './types.ts'

export { tagSummary }

function requireTag(body: unknown, action: string): TagSummary {
	const tag = unwrapRecord(body, 'tag', action)
	const summary = tagSummary(tag)
	if (!summary) throw new Error(`${action}: Kit returned no tag.`)
	return summary
}

/**
 * List tags. Pass `maxItems` (default 100).
 * @example
 * import { listTags } from 'kody:@kody/kit/tags'
 * const tags = await listTags({ maxItems: 50 })
 */
export async function listTags(input: KitAuthInput & { maxItems?: number } = {}): Promise<Array<TagSummary>> {
	const items = await kitListAll<JsonRecord>({
		...input,
		path: '/tags',
		itemKey: 'tags',
		maxItems: clampInt(input.maxItems, 1, 1000, 100),
	})
	return items.map((item) => tagSummary(item)).filter((item): item is TagSummary => Boolean(item))
}

/**
 * Create a tag. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { createTag } from 'kody:@kody/kit/tags'
 * const preview = await createTag({ name: 'waitlist', dryRun: true })
 */
export async function createTag(input: KitAuthInput & MutationInput & { name: string }) {
	const name = requireString(input.name, 'name')
	const preview = mutationPreview(input, {
		method: 'POST',
		path: '/tags',
		body: { name },
	})
	if (preview) return preview
	const result = await kitRequest({
		...input,
		method: 'POST',
		path: '/tags',
		body: { name },
		confirm: true,
	})
	return requireTag(result.data, 'createTag')
}

/**
 * Return the tag with this exact name, creating it when missing.
 * Requires `confirm: true` when a create is needed, or use `dryRun: true`.
 * @example
 * import { ensureTag } from 'kody:@kody/kit/tags'
 * const tag = await ensureTag({ name: 'waitlist', confirm: true })
 */
export async function ensureTag(input: KitAuthInput & MutationInput & { name: string }) {
	const name = requireString(input.name, 'name')
	const tags = await listTags(input)
	const existing = tags.find((tag) => tag.name === name)
	if (existing) return existing
	return createTag(input)
}

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

/**
 * Remove a tag from a subscriber. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { untagSubscriber } from 'kody:@kody/kit/tags'
 * const preview = await untagSubscriber({ tagId: 1, subscriberId: 2, dryRun: true })
 */
export async function untagSubscriber(
	input: KitAuthInput & MutationInput & { tagId: number | string; subscriberId: number | string },
) {
	const tagId = requireId(input.tagId, 'tagId')
	const subscriberId = requireId(input.subscriberId, 'subscriberId')
	const preview = mutationPreview(input, {
		method: 'DELETE',
		path: `/tags/${tagId}/subscribers/${subscriberId}`,
	})
	if (preview) return preview
	await kitRequest({
		...input,
		method: 'DELETE',
		path: `/tags/${tagId}/subscribers/${subscriberId}`,
		confirm: true,
	})
	return { tagId, subscriberId, untagged: true as const }
}

/**
 * List subscribers with a tag.
 * @example
 * import { listTagSubscribers } from 'kody:@kody/kit/tags'
 * const people = await listTagSubscribers({ tagId: 1, maxItems: 10 })
 */
export async function listTagSubscribers(
	input: KitAuthInput & { tagId: number | string; maxItems?: number },
) {
	const tagId = requireId(input.tagId, 'tagId')
	const items = await kitListAll<JsonRecord>({
		...input,
		path: `/tags/${tagId}/subscribers`,
		itemKey: 'subscribers',
		maxItems: clampInt(input.maxItems, 1, 500, 25),
	})
	return items
		.map((item) => subscriberSummary(item))
		.filter((item): item is NonNullable<ReturnType<typeof subscriberSummary>> => Boolean(item))
}

/**
 * Tag helpers. Defaults to `list`.
 * @example
 * import tags from 'kody:@kody/kit/tags'
 * const all = await tags({ action: 'list' })
 */
export default async function tagsEntrypoint(params: Record<string, unknown> = {}) {
	const input = requireRecord(params, 'tags')
	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 listTags({ ...auth, maxItems: input.maxItems as number | undefined })
		case 'create':
			return createTag({ ...auth, ...mutation, name: requireString(input.name, 'name') })
		case 'ensure':
			return ensureTag({ ...auth, ...mutation, name: requireString(input.name, 'name') })
		case 'tag':
			return tagSubscriber({
				...auth,
				...mutation,
				tagId: requireId(input.tagId, 'tagId'),
				email_address: requireString(input.email_address, 'email_address'),
			})
		case 'untag':
			return untagSubscriber({
				...auth,
				...mutation,
				tagId: requireId(input.tagId, 'tagId'),
				subscriberId: requireId(input.subscriberId, 'subscriberId'),
			})
		case 'subscribers':
			return listTagSubscribers({
				...auth,
				tagId: requireId(input.tagId, 'tagId'),
				maxItems: input.maxItems as number | undefined,
			})
		default:
			throw new Error('tags action must be one of: list, create, ensure, tag, untag, subscribers.')
	}
}

export { compactRecord }