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 }