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 } 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> { const maxItems = clampInt(input.maxItems, 1, 500, 25) const items = await kitListAll({ ...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({ ...input, method: 'GET', path: '/subscribers', query: { email_address: email }, }) const subscribers = Array.isArray(result.data.subscribers) ? (result.data.subscribers as Array) : [] 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 }, ) { 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 = {}, ) { 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 }