Skip to content

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

Package listing

@kody/zendesk

src/users.ts

253 lines · 7.6 KB · TypeScript
import { parseAuth } from './auth.ts'
import { zendeskRequest } from './client.ts'
import { compactDefined, extractListItems, mapUser } from './models.ts'
import type {
	DryRunResult,
	JsonRecord,
	MutationInput,
	ZendeskAuthInput,
	ZendeskPageInfo,
	ZendeskUser,
} from './types.ts'
import {
	clampInt,
	optionalBoolean,
	optionalNumber,
	optionalString,
	requireRecord,
	requireString,
} from './types.ts'

export type ListUsersInput = ZendeskAuthInput & {
	perPage?: number
	page?: number
	role?: string
}

export type GetUserInput = ZendeskAuthInput & {
	id: number | string
}

export type SearchUsersInput = ZendeskAuthInput & {
	query: string
	perPage?: number
	page?: number
}

export type CreateUserInput = ZendeskAuthInput &
	MutationInput & {
		name: string
		email?: string
		role?: string
		organizationId?: number
		externalId?: string
	}

export type UpdateUserInput = ZendeskAuthInput &
	MutationInput & {
		id: number | string
		name?: string
		email?: string
		role?: string
		organizationId?: number
		verified?: boolean
	}

export type UserListResult = {
	items: Array<ZendeskUser>
	pageInfo: ZendeskPageInfo
}

function userId(value: number | string, label = 'id'): string {
	if (typeof value === 'number' && Number.isInteger(value) && value > 0) return String(value)
	return requireString(value, label)
}

/**
 * List Zendesk users.
 * @example
 * import { listUsers } from 'kody:@kody/zendesk/users'
 * const { items } = await listUsers({ perPage: 10, subdomain: 'acme' })
 */
export async function listUsers(input: ListUsersInput = {}): Promise<UserListResult> {
	const result = await zendeskRequest<unknown>({
		...input,
		operation: 'users.read',
		path: '/users',
		query: compactDefined({
			per_page: clampInt(input.perPage, 1, 100, 20),
			page: input.page === undefined ? undefined : clampInt(input.page, 1, 10_000, 1, 'page'),
			role: input.role,
		}),
	})
	if ('dryRun' in result) throw new Error('listUsers is read-only.')
	return {
		items: extractListItems(result.data, ['users'])
			.map(mapUser)
			.filter((item): item is ZendeskUser => Boolean(item)),
		pageInfo: result.pageInfo,
	}
}

/**
 * Get one Zendesk user by id.
 * @example
 * import { getUser } from 'kody:@kody/zendesk/users'
 * const user = await getUser({ id, subdomain: 'acme' })
 */
export async function getUser(input: GetUserInput): Promise<ZendeskUser> {
	const id = userId(input.id)
	const result = await zendeskRequest<unknown>({
		...input,
		operation: 'users.read',
		path: `/users/${encodeURIComponent(id)}`,
	})
	if ('dryRun' in result) throw new Error('getUser is read-only.')
	const mapped = mapUser(result.data)
	if (!mapped) throw new Error('Zendesk did not return a user id.')
	return mapped
}

/**
 * Search Zendesk users. GET `/users/search` is a read.
 * @example
 * import { searchUsers } from 'kody:@kody/zendesk/users'
 * const { items } = await searchUsers({ query: 'pat@example.com', subdomain: 'acme' })
 */
export async function searchUsers(input: SearchUsersInput): Promise<UserListResult> {
	const result = await zendeskRequest<unknown>({
		...input,
		operation: 'users.read',
		path: '/users/search',
		query: compactDefined({
			query: requireString(input.query, 'query'),
			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('searchUsers is read-only.')
	return {
		items: extractListItems(result.data, ['users'])
			.map(mapUser)
			.filter((item): item is ZendeskUser => Boolean(item)),
		pageInfo: result.pageInfo,
	}
}

function userBody(input: CreateUserInput | UpdateUserInput): JsonRecord {
	return {
		user: compactDefined({
			name: 'name' in input ? input.name : undefined,
			email: input.email,
			role: input.role,
			organization_id: input.organizationId,
			external_id: 'externalId' in input ? input.externalId : undefined,
			verified: 'verified' in input ? input.verified : undefined,
		}),
	}
}

/**
 * Create a Zendesk user. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { createUser } from 'kody:@kody/zendesk/users'
 * const preview = await createUser({ name: 'Pat', email: 'pat@example.com', dryRun: true, subdomain: 'acme' })
 */
export async function createUser(input: CreateUserInput): Promise<ZendeskUser | DryRunResult> {
	const result = await zendeskRequest<unknown>({
		...input,
		operation: 'users.write',
		method: 'POST',
		path: '/users',
		body: userBody({ ...input, name: requireString(input.name, 'name') }),
	})
	if ('dryRun' in result) return result
	const mapped = mapUser(result.data)
	if (!mapped) throw new Error('Zendesk did not return a created user id.')
	return mapped
}

/**
 * Update a Zendesk user. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { updateUser } from 'kody:@kody/zendesk/users'
 * const preview = await updateUser({ id, name: 'Pat Lee', dryRun: true, subdomain: 'acme' })
 */
export async function updateUser(input: UpdateUserInput): Promise<ZendeskUser | DryRunResult> {
	const id = userId(input.id)
	const result = await zendeskRequest<unknown>({
		...input,
		operation: 'users.write',
		method: 'PUT',
		path: `/users/${encodeURIComponent(id)}`,
		body: userBody(input),
	})
	if ('dryRun' in result) return result
	const mapped = mapUser(result.data)
	if (!mapped) throw new Error('Zendesk did not return an updated user id.')
	return mapped
}

export function parseListUsers(params: Record<string, unknown>): ListUsersInput {
	const input = requireRecord(params, 'list-users')
	return {
		...parseAuth(input),
		perPage: input.perPage as number | undefined,
		page: input.page as number | undefined,
		role: optionalString(input.role, 'role'),
	}
}

export function parseGetUser(params: Record<string, unknown>): GetUserInput {
	const input = requireRecord(params, 'get-user')
	return { ...parseAuth(input), id: input.id as number | string }
}

export function parseSearchUsers(params: Record<string, unknown>): SearchUsersInput {
	const input = requireRecord(params, 'search-users')
	return {
		...parseAuth(input),
		query: requireString(input.query, 'query'),
		perPage: input.perPage as number | undefined,
		page: input.page as number | undefined,
	}
}

export function parseCreateUser(params: Record<string, unknown>): CreateUserInput {
	const input = requireRecord(params, 'create-user')
	return {
		...parseAuth(input),
		name: requireString(input.name, 'name'),
		email: optionalString(input.email, 'email'),
		role: optionalString(input.role, 'role'),
		organizationId: optionalNumber(input.organizationId, 'organizationId'),
		externalId: optionalString(input.externalId, 'externalId'),
		confirm: optionalBoolean(input.confirm, 'confirm'),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
	}
}

export function parseUpdateUser(params: Record<string, unknown>): UpdateUserInput {
	const input = requireRecord(params, 'update-user')
	return {
		...parseAuth(input),
		id: input.id as number | string,
		name: optionalString(input.name, 'name'),
		email: optionalString(input.email, 'email'),
		role: optionalString(input.role, 'role'),
		organizationId: optionalNumber(input.organizationId, 'organizationId'),
		verified: optionalBoolean(input.verified, 'verified'),
		confirm: optionalBoolean(input.confirm, 'confirm'),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
	}
}

/**
 * Zendesk user helpers. Writes require `confirm: true` or `dryRun: true`.
 * @example
 * import { listUsers } from 'kody:@kody/zendesk/users'
 * const { items } = await listUsers({ perPage: 10, subdomain: 'acme' })
 */
export default async function usersEntrypoint(params: Record<string, unknown> = {}) {
	return listUsers(parseListUsers(params))
}