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.')
}
}