Skip to content

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

Package listing

@kody/intercom

src/articles.ts

239 lines · 7.0 KB · TypeScript
import { parseAuth } from './auth.ts'
import { intercomRequest } from './client.ts'
import { compactDefined, extractListItems, mapArticle } from './models.ts'
import { getMe } from './me.ts'
import type {
	DryRunResult,
	IntercomArticle,
	IntercomAuthInput,
	IntercomPageInfo,
	JsonRecord,
	MutationInput,
} from './types.ts'
import {
	clampInt,
	optionalBoolean,
	optionalString,
	requireRecord,
	requireString,
} from './types.ts'

export type ListArticlesInput = IntercomAuthInput & {
	perPage?: number
	page?: number
}

export type GetArticleInput = IntercomAuthInput & {
	id: string
}

export type CreateArticleInput = IntercomAuthInput &
	MutationInput & {
		title: string
		body?: string
		authorId?: string
		state?: string
		parentId?: number | string
		parentType?: string
	}

export type UpdateArticleInput = IntercomAuthInput &
	MutationInput & {
		id: string
		title?: string
		body?: string
		authorId?: string
		state?: string
		parentId?: number | string
		parentType?: string
	}

export type ArticleListResult = {
	items: Array<IntercomArticle>
	pageInfo: IntercomPageInfo
}

async function resolveAuthorId(input: IntercomAuthInput & { authorId?: string }): Promise<string> {
	if (input.authorId) return requireString(input.authorId, 'authorId')
	const me = await getMe(input)
	return me.id
}

function articleBody(
	input: CreateArticleInput | UpdateArticleInput,
	authorId: string,
): JsonRecord {
	return compactDefined({
		title: 'title' in input ? input.title : undefined,
		body: input.body,
		author_id: authorId,
		state: input.state,
		parent_id: input.parentId,
		parent_type: input.parentType,
	})
}

/**
 * List Intercom Help Center articles.
 * @example
 * import { listArticles } from 'kody:@kody/intercom/articles'
 * const { items } = await listArticles({ perPage: 10 })
 */
export async function listArticles(input: ListArticlesInput = {}): Promise<ArticleListResult> {
	const result = await intercomRequest<unknown>({
		...input,
		operation: 'articles.read',
		path: '/articles',
		query: compactDefined({
			per_page: clampInt(input.perPage, 1, 150, 20),
			page: input.page === undefined ? undefined : clampInt(input.page, 1, 10_000, 1, 'page'),
		}),
	})
	if ('dryRun' in result) {
		throw new Error('listArticles is read-only.')
	}
	return {
		items: extractListItems(result.data)
			.map(mapArticle)
			.filter((item): item is IntercomArticle => Boolean(item)),
		pageInfo: result.pageInfo,
	}
}

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

/**
 * Create an Intercom article. Requires `confirm: true`, or use `dryRun: true`.
 * When `authorId` is omitted, the connected `/me` admin is used.
 * @example
 * import { createArticle } from 'kody:@kody/intercom/articles'
 * const preview = await createArticle({ title: 'Reset your password', dryRun: true })
 */
export async function createArticle(
	input: CreateArticleInput,
): Promise<IntercomArticle | DryRunResult> {
	const title = requireString(input.title, 'title')
	const authorId = input.dryRun
		? input.authorId ?? '<connected-admin>'
		: await resolveAuthorId(input)
	const result = await intercomRequest<unknown>({
		...input,
		operation: 'articles.write',
		method: 'POST',
		path: '/articles',
		body: articleBody({ ...input, title }, authorId),
	})
	if ('dryRun' in result) return result
	const mapped = mapArticle(result.data)
	if (!mapped) throw new Error('Intercom did not return a created article id.')
	return mapped
}

/**
 * Update an Intercom article. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { updateArticle } from 'kody:@kody/intercom/articles'
 * const preview = await updateArticle({ id, title: 'Updated title', dryRun: true })
 */
export async function updateArticle(
	input: UpdateArticleInput,
): Promise<IntercomArticle | DryRunResult> {
	const id = requireString(input.id, 'id')
	const authorId = input.dryRun
		? input.authorId ?? '<connected-admin>'
		: input.authorId
			? requireString(input.authorId, 'authorId')
			: undefined
	const result = await intercomRequest<unknown>({
		...input,
		operation: 'articles.write',
		method: 'PUT',
		path: `/articles/${encodeURIComponent(id)}`,
		body: compactDefined({
			title: input.title,
			body: input.body,
			author_id: authorId,
			state: input.state,
			parent_id: input.parentId,
			parent_type: input.parentType,
		}),
	})
	if ('dryRun' in result) return result
	const mapped = mapArticle(result.data)
	if (!mapped) throw new Error('Intercom did not return an updated article id.')
	return mapped
}

export function parseListArticles(params: Record<string, unknown>): ListArticlesInput {
	const input = requireRecord(params, 'list-articles')
	return {
		...parseAuth(input),
		perPage: input.perPage as number | undefined,
		page: input.page as number | undefined,
	}
}

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

export function parseCreateArticle(params: Record<string, unknown>): CreateArticleInput {
	const input = requireRecord(params, 'create-article')
	return {
		...parseAuth(input),
		title: requireString(input.title, 'title'),
		body: optionalString(input.body, 'body'),
		authorId: optionalString(input.authorId, 'authorId'),
		state: optionalString(input.state, 'state'),
		parentId: input.parentId as number | string | undefined,
		parentType: optionalString(input.parentType, 'parentType'),
		confirm: optionalBoolean(input.confirm, 'confirm'),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
	}
}

export function parseUpdateArticle(params: Record<string, unknown>): UpdateArticleInput {
	const input = requireRecord(params, 'update-article')
	return {
		...parseAuth(input),
		id: requireString(input.id, 'id'),
		title: optionalString(input.title, 'title'),
		body: optionalString(input.body, 'body'),
		authorId: optionalString(input.authorId, 'authorId'),
		state: optionalString(input.state, 'state'),
		parentId: input.parentId as number | string | undefined,
		parentType: optionalString(input.parentType, 'parentType'),
		confirm: optionalBoolean(input.confirm, 'confirm'),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
	}
}

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