Skip to content

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

Package listing

@kody/intercom

src/contacts.ts

286 lines · 8.4 KB · TypeScript
import { parseAuth } from './auth.ts'
import { intercomRequest } from './client.ts'
import { compactDefined, extractListItems, mapContact } from './models.ts'
import type {
	DryRunResult,
	IntercomAuthInput,
	IntercomContact,
	IntercomPageInfo,
	IntercomSearchQuery,
	JsonRecord,
	MutationInput,
} from './types.ts'
import {
	clampInt,
	optionalBoolean,
	optionalString,
	requireRecord,
	requireString,
} from './types.ts'

export type ListContactsInput = IntercomAuthInput & {
	perPage?: number
	startingAfter?: string
}

export type GetContactInput = IntercomAuthInput & {
	id: string
}

export type SearchContactsInput = IntercomAuthInput & {
	query?: IntercomSearchQuery | JsonRecord
	email?: string
	perPage?: number
	startingAfter?: string
}

export type CreateContactInput = IntercomAuthInput &
	MutationInput & {
		email?: string
		externalId?: string
		role?: string
		name?: string
		phone?: string
		signedUpAt?: number
		customAttributes?: JsonRecord
		body?: JsonRecord
	}

export type UpdateContactInput = IntercomAuthInput &
	MutationInput & {
		id: string
		email?: string
		externalId?: string
		name?: string
		phone?: string
		customAttributes?: JsonRecord
		body?: JsonRecord
	}

export type ContactListResult = {
	items: Array<IntercomContact>
	pageInfo: IntercomPageInfo
}

function searchBody(input: SearchContactsInput): JsonRecord {
	if (input.query) return { query: input.query }
	if (input.email) {
		return {
			query: {
				field: 'email',
				operator: '=',
				value: input.email,
			},
		}
	}
	throw new Error('searchContacts requires query or email.')
}

function contactWriteBody(input: CreateContactInput | UpdateContactInput): JsonRecord {
	if (input.body) return input.body
	return compactDefined({
		email: input.email,
		external_id: 'externalId' in input ? input.externalId : undefined,
		role: 'role' in input ? input.role : undefined,
		name: input.name,
		phone: input.phone,
		signed_up_at: 'signedUpAt' in input ? input.signedUpAt : undefined,
		custom_attributes: input.customAttributes,
	})
}

/**
 * List Intercom contacts.
 * @example
 * import { listContacts } from 'kody:@kody/intercom/contacts'
 * const { items } = await listContacts({ perPage: 10 })
 */
export async function listContacts(input: ListContactsInput = {}): Promise<ContactListResult> {
	const result = await intercomRequest<unknown>({
		...input,
		operation: 'contacts.read',
		path: '/contacts',
		query: compactDefined({
			per_page: clampInt(input.perPage, 1, 150, 20),
			starting_after: input.startingAfter,
		}),
	})
	if ('dryRun' in result) {
		throw new Error('listContacts is read-only.')
	}
	return {
		items: extractListItems(result.data)
			.map(mapContact)
			.filter((item): item is IntercomContact => Boolean(item)),
		pageInfo: result.pageInfo,
	}
}

/**
 * Get one Intercom contact by id.
 * @example
 * import { getContact } from 'kody:@kody/intercom/contacts'
 * const contact = await getContact({ id })
 */
export async function getContact(input: GetContactInput): Promise<IntercomContact> {
	const id = requireString(input.id, 'id')
	const result = await intercomRequest<unknown>({
		...input,
		operation: 'contacts.read',
		path: `/contacts/${encodeURIComponent(id)}`,
	})
	if ('dryRun' in result) {
		throw new Error('getContact is read-only.')
	}
	const mapped = mapContact(result.data)
	if (!mapped) throw new Error('Intercom did not return a contact id.')
	return mapped
}

/**
 * Search Intercom contacts. POST `/contacts/search` is a read.
 * @example
 * import { searchContacts } from 'kody:@kody/intercom/contacts'
 * const { items } = await searchContacts({ email: 'pat@example.com' })
 */
export async function searchContacts(input: SearchContactsInput): Promise<ContactListResult> {
	const result = await intercomRequest<unknown>({
		...input,
		operation: 'contacts.read',
		method: 'POST',
		path: '/contacts/search',
		body: compactDefined({
			...searchBody(input),
			pagination: compactDefined({
				per_page: input.perPage === undefined ? undefined : clampInt(input.perPage, 1, 150, 20),
				starting_after: input.startingAfter,
			}),
		}),
	})
	if ('dryRun' in result) {
		throw new Error('searchContacts is read-only.')
	}
	return {
		items: extractListItems(result.data)
			.map(mapContact)
			.filter((item): item is IntercomContact => Boolean(item)),
		pageInfo: result.pageInfo,
	}
}

/**
 * Create an Intercom contact. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { createContact } from 'kody:@kody/intercom/contacts'
 * const preview = await createContact({ email: 'pat@example.com', dryRun: true })
 */
export async function createContact(
	input: CreateContactInput,
): Promise<IntercomContact | DryRunResult> {
	const body = contactWriteBody(input)
	if (!body.email && !body.external_id && !input.body) {
		throw new Error('createContact requires email, externalId, or body.')
	}
	const result = await intercomRequest<unknown>({
		...input,
		operation: 'contacts.write',
		method: 'POST',
		path: '/contacts',
		body,
	})
	if ('dryRun' in result) return result
	const mapped = mapContact(result.data)
	if (!mapped) throw new Error('Intercom did not return a created contact id.')
	return mapped
}

/**
 * Update an Intercom contact. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { updateContact } from 'kody:@kody/intercom/contacts'
 * const preview = await updateContact({ id, name: 'Pat', dryRun: true })
 */
export async function updateContact(
	input: UpdateContactInput,
): Promise<IntercomContact | DryRunResult> {
	const id = requireString(input.id, 'id')
	const result = await intercomRequest<unknown>({
		...input,
		operation: 'contacts.write',
		method: 'PUT',
		path: `/contacts/${encodeURIComponent(id)}`,
		body: contactWriteBody(input),
	})
	if ('dryRun' in result) return result
	const mapped = mapContact(result.data)
	if (!mapped) throw new Error('Intercom did not return an updated contact id.')
	return mapped
}

export function parseListContacts(params: Record<string, unknown>): ListContactsInput {
	const input = requireRecord(params, 'list-contacts')
	return {
		...parseAuth(input),
		perPage: input.perPage as number | undefined,
		startingAfter: optionalString(input.startingAfter, 'startingAfter'),
	}
}

export function parseGetContact(params: Record<string, unknown>): GetContactInput {
	const input = requireRecord(params, 'get-contact')
	return { ...parseAuth(input), id: requireString(input.id, 'id') }
}

export function parseSearchContacts(params: Record<string, unknown>): SearchContactsInput {
	const input = requireRecord(params, 'search-contacts')
	return {
		...parseAuth(input),
		query: input.query as SearchContactsInput['query'],
		email: optionalString(input.email, 'email'),
		perPage: input.perPage as number | undefined,
		startingAfter: optionalString(input.startingAfter, 'startingAfter'),
	}
}

export function parseCreateContact(params: Record<string, unknown>): CreateContactInput {
	const input = requireRecord(params, 'create-contact')
	return {
		...parseAuth(input),
		email: optionalString(input.email, 'email'),
		externalId: optionalString(input.externalId, 'externalId'),
		role: optionalString(input.role, 'role'),
		name: optionalString(input.name, 'name'),
		phone: optionalString(input.phone, 'phone'),
		signedUpAt: input.signedUpAt as number | undefined,
		customAttributes: input.customAttributes as JsonRecord | undefined,
		body: input.body as JsonRecord | undefined,
		confirm: optionalBoolean(input.confirm, 'confirm'),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
	}
}

export function parseUpdateContact(params: Record<string, unknown>): UpdateContactInput {
	const input = requireRecord(params, 'update-contact')
	return {
		...parseAuth(input),
		id: requireString(input.id, 'id'),
		email: optionalString(input.email, 'email'),
		externalId: optionalString(input.externalId, 'externalId'),
		name: optionalString(input.name, 'name'),
		phone: optionalString(input.phone, 'phone'),
		customAttributes: input.customAttributes as JsonRecord | undefined,
		body: input.body as JsonRecord | undefined,
		confirm: optionalBoolean(input.confirm, 'confirm'),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
	}
}

/**
 * Intercom contact helpers. Writes require `confirm: true` or `dryRun: true`.
 * @example
 * import { listContacts } from 'kody:@kody/intercom/contacts'
 * const { items } = await listContacts({ perPage: 10 })
 */
export default async function contactsEntrypoint(params: Record<string, unknown> = {}) {
	return listContacts(parseListContacts(params))
}