import { parseAuth } from './auth.ts'
import { zendeskRequest } from './client.ts'
import { compactDefined, extractListItems, mapArticle } from './models.ts'
import type {
DryRunResult,
JsonRecord,
MutationInput,
ZendeskArticle,
ZendeskAuthInput,
ZendeskPageInfo,
} from './types.ts'
import {
clampInt,
optionalBoolean,
optionalNumber,
optionalString,
requireRecord,
requireString,
} from './types.ts'
export type ListArticlesInput = ZendeskAuthInput & {
perPage?: number
page?: number
locale?: string
sectionId?: number
}
export type GetArticleInput = ZendeskAuthInput & {
id: number | string
}
export type SearchArticlesInput = ZendeskAuthInput & {
query: string
locale?: string
sectionId?: number
perPage?: number
page?: number
}
export type CreateArticleInput = ZendeskAuthInput &
MutationInput & {
title: string
sectionId: number | string
body?: string
locale?: string
draft?: boolean
permissionGroupId?: number
userSegmentId?: number | null
labelNames?: Array<string>
}
export type UpdateArticleInput = ZendeskAuthInput &
MutationInput & {
id: number | string
title?: string
body?: string
locale?: string
draft?: boolean
permissionGroupId?: number
userSegmentId?: number | null
labelNames?: Array<string>
}
export type ArticleListResult = {
items: Array<ZendeskArticle>
pageInfo: ZendeskPageInfo
}
function articleId(value: number | string, label = 'id'): string {
if (typeof value === 'number' && Number.isInteger(value) && value > 0) return String(value)
return requireString(value, label)
}
/**
* List Zendesk Help Center articles.
* @example
* import { listArticles } from 'kody:@kody/zendesk/articles'
* const { items } = await listArticles({ perPage: 10, subdomain: 'acme' })
*/
export async function listArticles(input: ListArticlesInput = {}): Promise<ArticleListResult> {
const locale = optionalString(input.locale, 'locale')
const path = locale
? `/help_center/${encodeURIComponent(locale)}/articles`
: '/help_center/articles'
const result = await zendeskRequest<unknown>({
...input,
operation: 'articles.read',
path,
query: compactDefined({
per_page: clampInt(input.perPage, 1, 100, 20),
page: input.page === undefined ? undefined : clampInt(input.page, 1, 10_000, 1, 'page'),
section: input.sectionId,
}),
})
if ('dryRun' in result) throw new Error('listArticles is read-only.')
return {
items: extractListItems(result.data, ['articles'])
.map(mapArticle)
.filter((item): item is ZendeskArticle => Boolean(item)),
pageInfo: result.pageInfo,
}
}
/**
* Get one Zendesk Help Center article by id.
* @example
* import { getArticle } from 'kody:@kody/zendesk/articles'
* const article = await getArticle({ id, subdomain: 'acme' })
*/
export async function getArticle(input: GetArticleInput): Promise<ZendeskArticle> {
const id = articleId(input.id)
const result = await zendeskRequest<unknown>({
...input,
operation: 'articles.read',
path: `/help_center/articles/${encodeURIComponent(id)}`,
})
if ('dryRun' in result) throw new Error('getArticle is read-only.')
const mapped = mapArticle(result.data)
if (!mapped) throw new Error('Zendesk did not return an article id.')
return mapped
}
/**
* Search Zendesk Help Center articles. GET search is a read.
* @example
* import { searchArticles } from 'kody:@kody/zendesk/articles'
* const { items } = await searchArticles({ query: 'password reset', subdomain: 'acme' })
*/
export async function searchArticles(input: SearchArticlesInput): Promise<ArticleListResult> {
const result = await zendeskRequest<unknown>({
...input,
operation: 'articles.read',
path: '/help_center/articles/search',
query: compactDefined({
query: requireString(input.query, 'query'),
locale: input.locale,
section: input.sectionId,
per_page: clampInt(input.perPage, 1, 100, 20),
page: input.page === undefined ? undefined : clampInt(input.page, 1, 10_000, 1, 'page'),
}),
})
if ('dryRun' in result) throw new Error('searchArticles is read-only.')
return {
items: extractListItems(result.data, ['results', 'articles'])
.map(mapArticle)
.filter((item): item is ZendeskArticle => Boolean(item)),
pageInfo: result.pageInfo,
}
}
function createArticleBody(input: CreateArticleInput): JsonRecord {
return {
article: compactDefined({
title: requireString(input.title, 'title'),
body: input.body,
locale: input.locale ?? 'en-us',
draft: input.draft,
permission_group_id: input.permissionGroupId,
user_segment_id: input.userSegmentId,
label_names: input.labelNames,
}),
}
}
/**
* Create a Help Center article. Requires `confirm: true`, or use `dryRun: true`.
* @example
* import { createArticle } from 'kody:@kody/zendesk/articles'
* const preview = await createArticle({ title: 'Reset your password', sectionId: 1, dryRun: true, subdomain: 'acme' })
*/
export async function createArticle(
input: CreateArticleInput,
): Promise<ZendeskArticle | DryRunResult> {
const sectionId = articleId(input.sectionId, 'sectionId')
const result = await zendeskRequest<unknown>({
...input,
operation: 'articles.write',
method: 'POST',
path: `/help_center/sections/${encodeURIComponent(sectionId)}/articles`,
body: createArticleBody(input),
})
if ('dryRun' in result) return result
const mapped = mapArticle(result.data)
if (!mapped) throw new Error('Zendesk did not return a created article id.')
return mapped
}
/**
* Update a Help Center article. Requires `confirm: true`, or use `dryRun: true`.
* When `locale` plus title/body is set, updates that translation.
* @example
* import { updateArticle } from 'kody:@kody/zendesk/articles'
* const preview = await updateArticle({ id, title: 'Updated title', dryRun: true, subdomain: 'acme' })
*/
export async function updateArticle(
input: UpdateArticleInput,
): Promise<ZendeskArticle | DryRunResult> {
const id = articleId(input.id)
const locale = optionalString(input.locale, 'locale')
const translationUpdate = Boolean(locale && (input.title || input.body || input.draft !== undefined))
const path = translationUpdate
? `/help_center/articles/${encodeURIComponent(id)}/translations/${encodeURIComponent(locale as string)}`
: `/help_center/articles/${encodeURIComponent(id)}`
const body = translationUpdate
? {
translation: compactDefined({
title: input.title,
body: input.body,
draft: input.draft,
}),
}
: {
article: compactDefined({
title: input.title,
draft: input.draft,
permission_group_id: input.permissionGroupId,
user_segment_id: input.userSegmentId,
label_names: input.labelNames,
}),
}
const result = await zendeskRequest<unknown>({
...input,
operation: 'articles.write',
method: 'PUT',
path,
body,
})
if ('dryRun' in result) return result
const mapped = mapArticle(result.data)
if (!mapped) throw new Error('Zendesk 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,
locale: optionalString(input.locale, 'locale'),
sectionId: optionalNumber(input.sectionId, 'sectionId'),
}
}
export function parseGetArticle(params: Record<string, unknown>): GetArticleInput {
const input = requireRecord(params, 'get-article')
return { ...parseAuth(input), id: input.id as number | string }
}
export function parseSearchArticles(params: Record<string, unknown>): SearchArticlesInput {
const input = requireRecord(params, 'search-articles')
return {
...parseAuth(input),
query: requireString(input.query, 'query'),
locale: optionalString(input.locale, 'locale'),
sectionId: optionalNumber(input.sectionId, 'sectionId'),
perPage: input.perPage as number | undefined,
page: input.page as number | undefined,
}
}
export function parseCreateArticle(params: Record<string, unknown>): CreateArticleInput {
const input = requireRecord(params, 'create-article')
return {
...parseAuth(input),
title: requireString(input.title, 'title'),
sectionId: input.sectionId as number | string,
body: optionalString(input.body, 'body'),
locale: optionalString(input.locale, 'locale'),
draft: optionalBoolean(input.draft, 'draft'),
permissionGroupId: optionalNumber(input.permissionGroupId, 'permissionGroupId'),
userSegmentId: optionalNumber(input.userSegmentId, 'userSegmentId'),
labelNames: Array.isArray(input.labelNames)
? input.labelNames.map((name) => requireString(name, 'labelNames[]'))
: undefined,
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: input.id as number | string,
title: optionalString(input.title, 'title'),
body: optionalString(input.body, 'body'),
locale: optionalString(input.locale, 'locale'),
draft: optionalBoolean(input.draft, 'draft'),
permissionGroupId: optionalNumber(input.permissionGroupId, 'permissionGroupId'),
userSegmentId: optionalNumber(input.userSegmentId, 'userSegmentId'),
labelNames: Array.isArray(input.labelNames)
? input.labelNames.map((name) => requireString(name, 'labelNames[]'))
: undefined,
confirm: optionalBoolean(input.confirm, 'confirm'),
dryRun: optionalBoolean(input.dryRun, 'dryRun'),
}
}
/**
* Zendesk article helpers. Writes require `confirm: true` or `dryRun: true`.
* @example
* import { listArticles } from 'kody:@kody/zendesk/articles'
* const { items } = await listArticles({ perPage: 10, subdomain: 'acme' })
*/
export default async function articlesEntrypoint(params: Record<string, unknown> = {}) {
return listArticles(parseListArticles(params))
}