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 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 { const result = await intercomRequest({ ...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 { const id = requireString(input.id, 'id') const result = await intercomRequest({ ...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 { const result = await intercomRequest({ ...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 { 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({ ...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 { const id = requireString(input.id, 'id') const result = await intercomRequest({ ...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): 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): GetContactInput { const input = requireRecord(params, 'get-contact') return { ...parseAuth(input), id: requireString(input.id, 'id') } } export function parseSearchContacts(params: Record): 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): 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): 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 = {}) { return listContacts(parseListContacts(params)) }