Skip to content

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

Package listing

@kody/trello

src/cards.ts

227 lines · 6.9 KB · TypeScript
import { isDryRunResult, trelloRequest } from './client.ts'
import { parseAuthInput } from './auth.ts'
import { CARD_FIELDS, cardSummary, mapMany, requireMapped } from './models.ts'
import type {
	DryRunResult,
	MutationInput,
	TrelloAuthInput,
	TrelloCardSummary,
} from './types.ts'
import {
	clampInt,
	compactRecord,
	optionalBoolean,
	optionalString,
	optionalStringArray,
	requireRecord,
	requireString,
} from './types.ts'

export type ListCardsInput = TrelloAuthInput & {
	boardId?: string
	listId?: string
	filter?: 'open' | 'closed' | 'all'
	limit?: number
}

export type GetCardInput = TrelloAuthInput & {
	cardId: string
}

export type CreateCardInput = TrelloAuthInput &
	MutationInput & {
		listId: string
		name: string
		desc?: string
		due?: string
		pos?: string | number
		idMembers?: Array<string>
	}

export type UpdateCardInput = TrelloAuthInput &
	MutationInput & {
		cardId: string
		name?: string
		desc?: string
		closed?: boolean
		listId?: string
		due?: string
		dueComplete?: boolean
		pos?: string | number
	}

function cardFilter(value: unknown): 'open' | 'closed' | 'all' {
	if (value === undefined || value === null || value === '') return 'open'
	const filter = requireString(value, 'filter')
	if (filter !== 'open' && filter !== 'closed' && filter !== 'all') {
		throw new Error('filter must be "open", "closed", or "all".')
	}
	return filter
}

function optionalPos(value: unknown, label: string): string | number | undefined {
	if (value === undefined || value === null) return undefined
	if (typeof value === 'number' && Number.isFinite(value)) return value
	return requireString(value, label)
}

/**
 * List cards on a caller-supplied board or list id.
 * @example
 * import { listCards } from 'kody:@kody/trello/cards'
 * const cards = await listCards({ listId, limit: 25 })
 */
export async function listCards(input: ListCardsInput): Promise<Array<TrelloCardSummary>> {
	const boardId = optionalString(input.boardId, 'boardId')
	const listId = optionalString(input.listId, 'listId')
	if (!boardId && !listId) {
		throw new Error('listCards requires boardId or listId.')
	}
	const limit = clampInt(input.limit, 1, 1000, 50)
	const path = listId ? `/lists/${listId}/cards` : `/boards/${boardId}/cards`
	const result = await trelloRequest<unknown>({
		...input,
		method: 'GET',
		path,
		query: compactRecord({
			filter: cardFilter(input.filter),
			fields: CARD_FIELDS,
			limit,
		}),
	})
	if (isDryRunResult(result)) return []
	return mapMany(result.data, cardSummary).slice(0, limit)
}

/**
 * Get one Trello card by id supplied by the caller.
 * @example
 * import { getCard } from 'kody:@kody/trello/cards'
 * const card = await getCard({ cardId })
 */
export async function getCard(input: GetCardInput): Promise<TrelloCardSummary> {
	const cardId = requireString(input.cardId, 'cardId')
	const result = await trelloRequest<Record<string, unknown>>({
		...input,
		method: 'GET',
		path: `/cards/${cardId}`,
		query: { fields: CARD_FIELDS },
	})
	if (isDryRunResult(result)) {
		throw new Error('getCard is read-only and does not support dryRun.')
	}
	return requireMapped(cardSummary(result.data), 'getCard')
}

/**
 * Create a Trello card. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { createCard } from 'kody:@kody/trello/cards'
 * const preview = await createCard({ listId, name: 'Write README', dryRun: true })
 */
export async function createCard(input: CreateCardInput): Promise<TrelloCardSummary | DryRunResult> {
	const body = compactRecord({
		idList: requireString(input.listId, 'listId'),
		name: requireString(input.name, 'name'),
		desc: optionalString(input.desc, 'desc'),
		due: optionalString(input.due, 'due'),
		pos: optionalPos(input.pos, 'pos'),
		idMembers: optionalStringArray(input.idMembers, 'idMembers')?.join(','),
	})
	const result = await trelloRequest<Record<string, unknown>>({
		...input,
		method: 'POST',
		path: '/cards',
		query: body,
		body,
	})
	if (isDryRunResult(result)) return result
	return requireMapped(cardSummary(result.data), 'createCard')
}

/**
 * Update a Trello card. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import { updateCard } from 'kody:@kody/trello/cards'
 * const preview = await updateCard({ cardId, listId, dryRun: true })
 */
export async function updateCard(input: UpdateCardInput): Promise<TrelloCardSummary | DryRunResult> {
	const cardId = requireString(input.cardId, 'cardId')
	const body = compactRecord({
		name: optionalString(input.name, 'name'),
		desc: optionalString(input.desc, 'desc'),
		closed: optionalBoolean(input.closed, 'closed'),
		idList: optionalString(input.listId, 'listId'),
		due: optionalString(input.due, 'due'),
		dueComplete: optionalBoolean(input.dueComplete, 'dueComplete'),
		pos: optionalPos(input.pos, 'pos'),
	})
	if (Object.keys(body).length === 0) {
		throw new Error(
			'updateCard requires at least one of name, desc, closed, listId, due, dueComplete, or pos.',
		)
	}
	const result = await trelloRequest<Record<string, unknown>>({
		...input,
		method: 'PUT',
		path: `/cards/${cardId}`,
		query: body,
		body,
	})
	if (isDryRunResult(result)) return result
	return requireMapped(cardSummary(result.data), 'updateCard')
}

export { cardSummary }

/**
 * Card helpers dispatcher.
 * @example
 * import cards from 'kody:@kody/trello/cards'
 * const items = await cards({ action: 'list', listId })
 */
export default async function cardsEntrypoint(params: Record<string, unknown> = {}) {
	const input = requireRecord(params, 'cards')
	const auth = parseAuthInput(input)
	const action = optionalString(input.action, 'action') ?? 'list'
	switch (action) {
		case 'list':
			return listCards({
				...auth,
				boardId: optionalString(input.boardId, 'boardId'),
				listId: optionalString(input.listId, 'listId'),
				filter: cardFilter(input.filter),
				limit: input.limit as number | undefined,
			})
		case 'get':
			return getCard({ ...auth, cardId: requireString(input.cardId, 'cardId') })
		case 'create':
			return createCard({
				...auth,
				listId: requireString(input.listId, 'listId'),
				name: requireString(input.name, 'name'),
				desc: optionalString(input.desc, 'desc'),
				due: optionalString(input.due, 'due'),
				pos: optionalPos(input.pos, 'pos'),
				idMembers: optionalStringArray(input.idMembers, 'idMembers'),
				confirm: optionalBoolean(input.confirm, 'confirm'),
				dryRun: optionalBoolean(input.dryRun, 'dryRun'),
			})
		case 'update':
			return updateCard({
				...auth,
				cardId: requireString(input.cardId, 'cardId'),
				name: optionalString(input.name, 'name'),
				desc: optionalString(input.desc, 'desc'),
				closed: optionalBoolean(input.closed, 'closed'),
				listId: optionalString(input.listId, 'listId'),
				due: optionalString(input.due, 'due'),
				dueComplete: optionalBoolean(input.dueComplete, 'dueComplete'),
				pos: optionalPos(input.pos, 'pos'),
				confirm: optionalBoolean(input.confirm, 'confirm'),
				dryRun: optionalBoolean(input.dryRun, 'dryRun'),
			})
		default:
			throw new Error('cards action must be one of: list, get, create, update.')
	}
}