import { parseAuth } from './auth.ts'
import { zendeskRequest } from './client.ts'
import { compactDefined, extractListItems, mapTicket, mapTicketComment } from './models.ts'
import type {
DryRunResult,
JsonRecord,
MutationInput,
ZendeskAuthInput,
ZendeskPageInfo,
ZendeskTicket,
ZendeskTicketComment,
} from './types.ts'
import {
clampInt,
optionalBoolean,
optionalNumber,
optionalString,
requireRecord,
requireString,
} from './types.ts'
export type ListTicketsInput = ZendeskAuthInput & {
perPage?: number
page?: number
sortBy?: string
sortOrder?: string
}
export type GetTicketInput = ZendeskAuthInput & {
id: number | string
}
export type SearchTicketsInput = ZendeskAuthInput & {
query: string
perPage?: number
page?: number
sortBy?: string
sortOrder?: string
}
export type CreateTicketInput = ZendeskAuthInput &
MutationInput & {
subject: string
comment: string
requesterId?: number
requesterEmail?: string
requesterName?: string
assigneeId?: number
priority?: string
type?: string
status?: string
tags?: Array<string>
}
export type UpdateTicketInput = ZendeskAuthInput &
MutationInput & {
id: number | string
subject?: string
status?: string
priority?: string
type?: string
assigneeId?: number
comment?: string
publicComment?: boolean
tags?: Array<string>
}
export type ListTicketCommentsInput = ZendeskAuthInput & {
id: number | string
perPage?: number
page?: number
}
export type AddTicketCommentInput = ZendeskAuthInput &
MutationInput & {
id: number | string
comment: string
publicComment?: boolean
}
export type TicketListResult = {
items: Array<ZendeskTicket>
pageInfo: ZendeskPageInfo
}
export type TicketCommentListResult = {
items: Array<ZendeskTicketComment>
pageInfo: ZendeskPageInfo
}
function ticketId(value: number | string, label = 'id'): string {
if (typeof value === 'number' && Number.isInteger(value) && value > 0) return String(value)
return requireString(value, label)
}
function searchQuery(query: string): string {
const trimmed = requireString(query, 'query')
return /\btype:\s*ticket\b/i.test(trimmed) ? trimmed : `type:ticket ${trimmed}`
}
/**
* List Zendesk tickets.
* @example
* import { listTickets } from 'kody:@kody/zendesk/tickets'
* const { items } = await listTickets({ perPage: 10, subdomain: 'acme' })
*/
export async function listTickets(input: ListTicketsInput = {}): Promise<TicketListResult> {
const result = await zendeskRequest<unknown>({
...input,
operation: 'tickets.read',
path: '/tickets',
query: compactDefined({
per_page: clampInt(input.perPage, 1, 100, 20),
page: input.page === undefined ? undefined : clampInt(input.page, 1, 10_000, 1, 'page'),
sort_by: input.sortBy,
sort_order: input.sortOrder,
}),
})
if ('dryRun' in result) throw new Error('listTickets is read-only.')
return {
items: extractListItems(result.data, ['tickets'])
.map(mapTicket)
.filter((item): item is ZendeskTicket => Boolean(item)),
pageInfo: result.pageInfo,
}
}
/**
* Get one Zendesk ticket by id.
* @example
* import { getTicket } from 'kody:@kody/zendesk/tickets'
* const ticket = await getTicket({ id, subdomain: 'acme' })
*/
export async function getTicket(input: GetTicketInput): Promise<ZendeskTicket> {
const id = ticketId(input.id)
const result = await zendeskRequest<unknown>({
...input,
operation: 'tickets.read',
path: `/tickets/${encodeURIComponent(id)}`,
})
if ('dryRun' in result) throw new Error('getTicket is read-only.')
const mapped = mapTicket(result.data)
if (!mapped) throw new Error('Zendesk did not return a ticket id.')
return mapped
}
/**
* Search Zendesk tickets. GET `/search` is a read.
* @example
* import { searchTickets } from 'kody:@kody/zendesk/tickets'
* const { items } = await searchTickets({ query: 'status:open', subdomain: 'acme' })
*/
export async function searchTickets(input: SearchTicketsInput): Promise<TicketListResult> {
const result = await zendeskRequest<unknown>({
...input,
operation: 'tickets.read',
path: '/search',
query: compactDefined({
query: searchQuery(input.query),
per_page: clampInt(input.perPage, 1, 100, 20),
page: input.page === undefined ? undefined : clampInt(input.page, 1, 10_000, 1, 'page'),
sort_by: input.sortBy,
sort_order: input.sortOrder,
}),
})
if ('dryRun' in result) throw new Error('searchTickets is read-only.')
return {
items: extractListItems(result.data, ['results', 'tickets'])
.map(mapTicket)
.filter((item): item is ZendeskTicket => Boolean(item)),
pageInfo: result.pageInfo,
}
}
function createTicketBody(input: CreateTicketInput): JsonRecord {
const requester =
input.requesterId !== undefined
? undefined
: input.requesterEmail || input.requesterName
? compactDefined({
email: input.requesterEmail,
name: input.requesterName,
})
: undefined
return {
ticket: compactDefined({
subject: requireString(input.subject, 'subject'),
comment: { body: requireString(input.comment, 'comment') },
requester_id: input.requesterId,
requester,
assignee_id: input.assigneeId,
priority: input.priority,
type: input.type,
status: input.status,
tags: input.tags,
}),
}
}
/**
* Create a Zendesk ticket. Requires `confirm: true`, or use `dryRun: true`.
* @example
* import { createTicket } from 'kody:@kody/zendesk/tickets'
* const preview = await createTicket({ subject: 'Reset password', comment: 'User cannot sign in.', dryRun: true, subdomain: 'acme' })
*/
export async function createTicket(
input: CreateTicketInput,
): Promise<ZendeskTicket | DryRunResult> {
const result = await zendeskRequest<unknown>({
...input,
operation: 'tickets.write',
method: 'POST',
path: '/tickets',
body: createTicketBody(input),
})
if ('dryRun' in result) return result
const mapped = mapTicket(result.data)
if (!mapped) throw new Error('Zendesk did not return a created ticket id.')
return mapped
}
function updateTicketBody(input: UpdateTicketInput | AddTicketCommentInput): JsonRecord {
const comment = 'comment' in input && input.comment
? compactDefined({
body: requireString(input.comment, 'comment'),
public: input.publicComment,
})
: undefined
return {
ticket: compactDefined({
subject: 'subject' in input ? input.subject : undefined,
status: 'status' in input ? input.status : undefined,
priority: 'priority' in input ? input.priority : undefined,
type: 'type' in input ? input.type : undefined,
assignee_id: 'assigneeId' in input ? input.assigneeId : undefined,
tags: 'tags' in input ? input.tags : undefined,
comment,
}),
}
}
/**
* Update a Zendesk ticket. Requires `confirm: true`, or use `dryRun: true`.
* @example
* import { updateTicket } from 'kody:@kody/zendesk/tickets'
* const preview = await updateTicket({ id, status: 'solved', dryRun: true, subdomain: 'acme' })
*/
export async function updateTicket(
input: UpdateTicketInput,
): Promise<ZendeskTicket | DryRunResult> {
const id = ticketId(input.id)
const result = await zendeskRequest<unknown>({
...input,
operation: 'tickets.write',
method: 'PUT',
path: `/tickets/${encodeURIComponent(id)}`,
body: updateTicketBody(input),
})
if ('dryRun' in result) return result
const mapped = mapTicket(result.data)
if (!mapped) throw new Error('Zendesk did not return an updated ticket id.')
return mapped
}
/**
* List comments on a Zendesk ticket.
* @example
* import { listTicketComments } from 'kody:@kody/zendesk/tickets'
* const { items } = await listTicketComments({ id, subdomain: 'acme' })
*/
export async function listTicketComments(
input: ListTicketCommentsInput,
): Promise<TicketCommentListResult> {
const id = ticketId(input.id)
const result = await zendeskRequest<unknown>({
...input,
operation: 'tickets.read',
path: `/tickets/${encodeURIComponent(id)}/comments`,
query: compactDefined({
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('listTicketComments is read-only.')
return {
items: extractListItems(result.data, ['comments'])
.map(mapTicketComment)
.filter((item): item is ZendeskTicketComment => Boolean(item)),
pageInfo: result.pageInfo,
}
}
/**
* Add a comment to a Zendesk ticket. Requires `confirm: true`, or use `dryRun: true`.
* @example
* import { addTicketComment } from 'kody:@kody/zendesk/tickets'
* const preview = await addTicketComment({ id, comment: 'Looking into this.', dryRun: true, subdomain: 'acme' })
*/
export async function addTicketComment(
input: AddTicketCommentInput,
): Promise<ZendeskTicket | DryRunResult> {
const id = ticketId(input.id)
const result = await zendeskRequest<unknown>({
...input,
operation: 'tickets.write',
method: 'PUT',
path: `/tickets/${encodeURIComponent(id)}`,
body: updateTicketBody(input),
})
if ('dryRun' in result) return result
const mapped = mapTicket(result.data)
if (!mapped) throw new Error('Zendesk did not return the updated ticket id.')
return mapped
}
export function parseListTickets(params: Record<string, unknown>): ListTicketsInput {
const input = requireRecord(params, 'list-tickets')
return {
...parseAuth(input),
perPage: input.perPage as number | undefined,
page: input.page as number | undefined,
sortBy: optionalString(input.sortBy, 'sortBy'),
sortOrder: optionalString(input.sortOrder, 'sortOrder'),
}
}
export function parseGetTicket(params: Record<string, unknown>): GetTicketInput {
const input = requireRecord(params, 'get-ticket')
return { ...parseAuth(input), id: input.id as number | string }
}
export function parseSearchTickets(params: Record<string, unknown>): SearchTicketsInput {
const input = requireRecord(params, 'search-tickets')
return {
...parseAuth(input),
query: requireString(input.query, 'query'),
perPage: input.perPage as number | undefined,
page: input.page as number | undefined,
sortBy: optionalString(input.sortBy, 'sortBy'),
sortOrder: optionalString(input.sortOrder, 'sortOrder'),
}
}
export function parseCreateTicket(params: Record<string, unknown>): CreateTicketInput {
const input = requireRecord(params, 'create-ticket')
return {
...parseAuth(input),
subject: requireString(input.subject, 'subject'),
comment: requireString(input.comment, 'comment'),
requesterId: optionalNumber(input.requesterId, 'requesterId'),
requesterEmail: optionalString(input.requesterEmail, 'requesterEmail'),
requesterName: optionalString(input.requesterName, 'requesterName'),
assigneeId: optionalNumber(input.assigneeId, 'assigneeId'),
priority: optionalString(input.priority, 'priority'),
type: optionalString(input.type, 'type'),
status: optionalString(input.status, 'status'),
tags: Array.isArray(input.tags) ? input.tags.map((tag) => requireString(tag, 'tags[]')) : undefined,
confirm: optionalBoolean(input.confirm, 'confirm'),
dryRun: optionalBoolean(input.dryRun, 'dryRun'),
}
}
export function parseUpdateTicket(params: Record<string, unknown>): UpdateTicketInput {
const input = requireRecord(params, 'update-ticket')
return {
...parseAuth(input),
id: input.id as number | string,
subject: optionalString(input.subject, 'subject'),
status: optionalString(input.status, 'status'),
priority: optionalString(input.priority, 'priority'),
type: optionalString(input.type, 'type'),
assigneeId: optionalNumber(input.assigneeId, 'assigneeId'),
comment: optionalString(input.comment, 'comment'),
publicComment: optionalBoolean(input.publicComment, 'publicComment'),
tags: Array.isArray(input.tags) ? input.tags.map((tag) => requireString(tag, 'tags[]')) : undefined,
confirm: optionalBoolean(input.confirm, 'confirm'),
dryRun: optionalBoolean(input.dryRun, 'dryRun'),
}
}
export function parseListTicketComments(params: Record<string, unknown>): ListTicketCommentsInput {
const input = requireRecord(params, 'list-ticket-comments')
return {
...parseAuth(input),
id: input.id as number | string,
perPage: input.perPage as number | undefined,
page: input.page as number | undefined,
}
}
export function parseAddTicketComment(params: Record<string, unknown>): AddTicketCommentInput {
const input = requireRecord(params, 'add-ticket-comment')
return {
...parseAuth(input),
id: input.id as number | string,
comment: requireString(input.comment, 'comment'),
publicComment: optionalBoolean(input.publicComment, 'publicComment'),
confirm: optionalBoolean(input.confirm, 'confirm'),
dryRun: optionalBoolean(input.dryRun, 'dryRun'),
}
}
/**
* Zendesk ticket helpers. Writes require `confirm: true` or `dryRun: true`.
* @example
* import { listTickets } from 'kody:@kody/zendesk/tickets'
* const { items } = await listTickets({ perPage: 10, subdomain: 'acme' })
*/
export default async function ticketsEntrypoint(params: Record<string, unknown> = {}) {
return listTickets(parseListTickets(params))
}