Skip to content

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

Package listing

@kody/resend

src/contacts.ts

415 lines · 12.3 KB · TypeScript
import {
	contactSummary,
	mutationPreview,
	parseAction,
	resendRequest,
	unwrapList,
	type MutationGuardInput,
	type ResendAuthOptions,
} from './resend-core.ts'

export type ListContactsInput = ResendAuthOptions & {
	limit?: number
	after?: string
	before?: string
	segment_id?: string
}

/** GET /contacts — audience contacts as compact summaries. */
export async function listContacts(input: ListContactsInput = {}) {
	const body = await resendRequest({
		path: '/contacts',
		query: {
			limit: input.limit,
			after: input.after,
			before: input.before,
			segment_id: input.segment_id,
		},
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
	return unwrapList(body).map((contact) => contactSummary(contact)).filter(Boolean)
}

/** GET /contacts/{id} — by id or email. */
export async function getContact(input: ResendAuthOptions & { idOrEmail: string }) {
	if (!input.idOrEmail) throw new Error('getContact: idOrEmail is required')
	return await resendRequest({
		path: '/contacts/' + encodeURIComponent(input.idOrEmail),
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

export type CreateContactInput = MutationGuardInput & {
	email: string
	first_name?: string
	last_name?: string
	unsubscribed?: boolean
	properties?: Record<string, unknown>
}

/** POST /contacts — add a person to the audience. */
export async function createContact(input: CreateContactInput) {
	if (!input.email) throw new Error('createContact: email is required')
	const body = {
		email: input.email,
		first_name: input.first_name,
		last_name: input.last_name,
		unsubscribed: input.unsubscribed,
		properties: input.properties,
	}
	const preview = mutationPreview(input, {
		action: 'create contact ' + input.email,
		method: 'POST',
		path: '/contacts',
		body,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/contacts',
		method: 'POST',
		body,
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

export type UpdateContactInput = MutationGuardInput & {
	idOrEmail: string
	first_name?: string
	last_name?: string
	unsubscribed?: boolean
	properties?: Record<string, unknown>
}

/** PATCH /contacts/{id} */
export async function updateContact(input: UpdateContactInput) {
	if (!input.idOrEmail) throw new Error('updateContact: idOrEmail is required')
	const body = {
		first_name: input.first_name,
		last_name: input.last_name,
		unsubscribed: input.unsubscribed,
		properties: input.properties,
	}
	const preview = mutationPreview(input, {
		action: 'update contact ' + input.idOrEmail,
		method: 'PATCH',
		path: '/contacts/' + input.idOrEmail,
		body,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/contacts/' + encodeURIComponent(input.idOrEmail),
		method: 'PATCH',
		body,
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** Mark a contact unsubscribed without deleting the record. */
export async function unsubscribeContact(input: MutationGuardInput & { idOrEmail: string }) {
	return await updateContact({ ...input, unsubscribed: true })
}

/** DELETE /contacts/{id}. Destructive; requires confirm: true. */
export async function deleteContact(input: MutationGuardInput & { idOrEmail: string }) {
	if (!input.idOrEmail) throw new Error('deleteContact: idOrEmail is required')
	const preview = mutationPreview(input, {
		action: 'delete contact ' + input.idOrEmail,
		method: 'DELETE',
		path: '/contacts/' + input.idOrEmail,
		requireConfirm: true,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/contacts/' + encodeURIComponent(input.idOrEmail),
		method: 'DELETE',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** GET /segments */
export async function listSegments(input: ResendAuthOptions = {}) {
	const body = await resendRequest({
		path: '/segments',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
	return unwrapList(body)
}

/** GET /segments/{id} */
export async function getSegment(input: ResendAuthOptions & { segmentId: string }) {
	if (!input.segmentId) throw new Error('getSegment: segmentId is required')
	return await resendRequest({
		path: '/segments/' + encodeURIComponent(input.segmentId),
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** POST /segments */
export async function createSegment(input: MutationGuardInput & { name: string }) {
	if (!input.name) throw new Error('createSegment: name is required')
	const body = { name: input.name }
	const preview = mutationPreview(input, {
		action: 'create segment ' + input.name,
		method: 'POST',
		path: '/segments',
		body,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/segments',
		method: 'POST',
		body,
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** DELETE /segments/{id}. Destructive; requires confirm: true. */
export async function deleteSegment(input: MutationGuardInput & { segmentId: string }) {
	if (!input.segmentId) throw new Error('deleteSegment: segmentId is required')
	const preview = mutationPreview(input, {
		action: 'delete segment ' + input.segmentId,
		method: 'DELETE',
		path: '/segments/' + input.segmentId,
		requireConfirm: true,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/segments/' + encodeURIComponent(input.segmentId),
		method: 'DELETE',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** GET /contacts/{id}/segments */
export async function listContactSegments(input: ResendAuthOptions & { idOrEmail: string }) {
	if (!input.idOrEmail) throw new Error('listContactSegments: idOrEmail is required')
	const body = await resendRequest({
		path: '/contacts/' + encodeURIComponent(input.idOrEmail) + '/segments',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
	return unwrapList(body)
}

/** POST /contacts/{id}/segments/{segment_id} */
export async function addContactToSegment(
	input: MutationGuardInput & { idOrEmail: string; segmentId: string },
) {
	if (!input.idOrEmail) throw new Error('addContactToSegment: idOrEmail is required')
	if (!input.segmentId) throw new Error('addContactToSegment: segmentId is required')
	const path = '/contacts/' + input.idOrEmail + '/segments/' + input.segmentId
	const preview = mutationPreview(input, {
		action: 'add contact ' + input.idOrEmail + ' to segment ' + input.segmentId,
		method: 'POST',
		path,
	})
	if (preview) return preview
	return await resendRequest({
		path:
			'/contacts/' +
			encodeURIComponent(input.idOrEmail) +
			'/segments/' +
			encodeURIComponent(input.segmentId),
		method: 'POST',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** DELETE /contacts/{id}/segments/{segment_id} */
export async function removeContactFromSegment(
	input: MutationGuardInput & { idOrEmail: string; segmentId: string },
) {
	if (!input.idOrEmail) throw new Error('removeContactFromSegment: idOrEmail is required')
	if (!input.segmentId) throw new Error('removeContactFromSegment: segmentId is required')
	const path = '/contacts/' + input.idOrEmail + '/segments/' + input.segmentId
	const preview = mutationPreview(input, {
		action: 'remove contact ' + input.idOrEmail + ' from segment ' + input.segmentId,
		method: 'DELETE',
		path,
	})
	if (preview) return preview
	return await resendRequest({
		path:
			'/contacts/' +
			encodeURIComponent(input.idOrEmail) +
			'/segments/' +
			encodeURIComponent(input.segmentId),
		method: 'DELETE',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** GET /topics */
export async function listTopics(input: ResendAuthOptions = {}) {
	const body = await resendRequest({
		path: '/topics',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
	return unwrapList(body)
}

/** GET /topics/{id} */
export async function getTopic(input: ResendAuthOptions & { topicId: string }) {
	if (!input.topicId) throw new Error('getTopic: topicId is required')
	return await resendRequest({
		path: '/topics/' + encodeURIComponent(input.topicId),
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** POST /topics */
export async function createTopic(input: MutationGuardInput & { name: string }) {
	if (!input.name) throw new Error('createTopic: name is required')
	const body = { name: input.name }
	const preview = mutationPreview(input, {
		action: 'create topic ' + input.name,
		method: 'POST',
		path: '/topics',
		body,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/topics',
		method: 'POST',
		body,
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

/** GET /contacts/{id}/topics */
export async function listContactTopics(input: ResendAuthOptions & { idOrEmail: string }) {
	if (!input.idOrEmail) throw new Error('listContactTopics: idOrEmail is required')
	const body = await resendRequest({
		path: '/contacts/' + encodeURIComponent(input.idOrEmail) + '/topics',
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
	return unwrapList(body)
}

export type UpdateContactTopicsInput = MutationGuardInput & {
	idOrEmail: string
	topics: Array<{ id: string; subscribed: boolean }>
}

/** PATCH /contacts/{id}/topics */
export async function updateContactTopics(input: UpdateContactTopicsInput) {
	if (!input.idOrEmail) throw new Error('updateContactTopics: idOrEmail is required')
	if (!Array.isArray(input.topics)) {
		throw new Error('updateContactTopics: pass an array of { id, subscribed }')
	}
	const body = { topics: input.topics }
	const preview = mutationPreview(input, {
		action: 'update contact topics ' + input.idOrEmail,
		method: 'PATCH',
		path: '/contacts/' + input.idOrEmail + '/topics',
		body,
	})
	if (preview) return preview
	return await resendRequest({
		path: '/contacts/' + encodeURIComponent(input.idOrEmail) + '/topics',
		method: 'PATCH',
		body,
		account: input.account,
		secretName: input.secretName,
		integration: input.integration,
	})
}

const contactActions = [
	'list-contacts',
	'get-contact',
	'create-contact',
	'update-contact',
	'unsubscribe-contact',
	'delete-contact',
	'list-segments',
	'get-segment',
	'create-segment',
	'delete-segment',
	'list-contact-segments',
	'add-contact-to-segment',
	'remove-contact-from-segment',
	'list-topics',
	'get-topic',
	'create-topic',
	'list-contact-topics',
	'update-contact-topics',
] as const

/** Audience dispatcher. Defaults to list-contacts. */
export default async function contacts(input: Record<string, unknown> = {}) {
	const action = parseAction(input.action, contactActions, 'list-contacts', 'contacts')
	switch (action) {
		case 'list-contacts':
			return await listContacts(input as ListContactsInput)
		case 'get-contact':
			return await getContact(input as never)
		case 'create-contact':
			return await createContact(input as CreateContactInput)
		case 'update-contact':
			return await updateContact(input as UpdateContactInput)
		case 'unsubscribe-contact':
			return await unsubscribeContact(input as never)
		case 'delete-contact':
			return await deleteContact(input as never)
		case 'list-segments':
			return await listSegments(input)
		case 'get-segment':
			return await getSegment(input as never)
		case 'create-segment':
			return await createSegment(input as never)
		case 'delete-segment':
			return await deleteSegment(input as never)
		case 'list-contact-segments':
			return await listContactSegments(input as never)
		case 'add-contact-to-segment':
			return await addContactToSegment(input as never)
		case 'remove-contact-from-segment':
			return await removeContactFromSegment(input as never)
		case 'list-topics':
			return await listTopics(input)
		case 'get-topic':
			return await getTopic(input as never)
		case 'create-topic':
			return await createTopic(input as never)
		case 'list-contact-topics':
			return await listContactTopics(input as never)
		case 'update-contact-topics':
			return await updateContactTopics(input as UpdateContactTopicsInput)
		default: {
			const exhaustive: never = action
			throw new Error('Unhandled contacts action: ' + String(exhaustive))
		}
	}
}