import { resolveTrelloAuth, trelloFetch, type ResolvedTrelloAuth } from './auth.ts'
import type { DryRunResult, JsonRecord, MutationInput, TrelloAuthInput } from './types.ts'
import {
TRELLO_API_BASE_URL,
isMutatingMethod,
mutationPreview,
nextStepForAuth,
normalizeTrelloPath,
trelloUrl,
} from './setup.ts'
export class TrelloApiError extends Error {
readonly status: number
readonly body: unknown
readonly method: string
readonly path: string
constructor(
message: string,
options: { status: number; body: unknown; method: string; path: string },
) {
super(message)
this.name = 'TrelloApiError'
this.status = options.status
this.body = options.body
this.method = options.method
this.path = options.path
}
}
export type TrelloRequestInput = TrelloAuthInput &
MutationInput & {
path: string
method?: string
query?: JsonRecord
body?: JsonRecord
}
export type TrelloResponse<T> = {
data: T
status: number
}
function trelloErrorMessage(body: unknown): string | null {
if (typeof body === 'string' && body.trim().length > 0) {
return body.trim().slice(0, 400)
}
if (!body || typeof body !== 'object') return null
const record = body as { message?: unknown; error?: unknown }
if (typeof record.message === 'string' && record.message.length > 0) return record.message
if (typeof record.error === 'string' && record.error.length > 0) return record.error
return null
}
export async function trelloRequest<T = unknown>(
input: TrelloRequestInput,
): Promise<TrelloResponse<T> | DryRunResult> {
const method = (input.method ?? 'GET').toUpperCase()
const path = normalizeTrelloPath(input.path)
if (isMutatingMethod(method)) {
const preview = mutationPreview(input, {
method,
path,
body: input.body,
})
if (preview) return preview
}
const auth = await resolveTrelloAuth(input)
return trelloRequestWithAuth<T>(auth, {
method,
path,
query: input.query,
body: input.body,
})
}
export async function trelloRequestWithAuth<T = unknown>(
auth: ResolvedTrelloAuth,
input: {
method: string
path: string
query?: JsonRecord
body?: JsonRecord
},
): Promise<TrelloResponse<T>> {
const url = trelloUrl(input.path, input.query)
const headers = new Headers({
Accept: 'application/json',
'User-Agent': 'kody-trello/1.0',
})
const init: RequestInit = { method: input.method, headers }
if (input.body !== undefined) {
headers.set('Content-Type', 'application/json')
init.body = JSON.stringify(input.body)
}
const response = await trelloFetch(auth, url, init)
const text = await response.text()
let parsed: unknown = text
try {
parsed = text ? JSON.parse(text) : null
} catch {
parsed = text
}
if (!response.ok) {
const firstMessage =
trelloErrorMessage(parsed) ||
(typeof parsed === 'string' ? parsed.slice(0, 400) : response.statusText)
throw new TrelloApiError(
[
`Trello ${input.method} ${input.path} failed (${response.status}): ${firstMessage}.`,
nextStepForAuth(auth),
].join(' '),
{
status: response.status,
body: parsed,
method: input.method,
path: input.path,
},
)
}
return {
data: parsed as T,
status: response.status,
}
}
export function isDryRunResult(value: unknown): value is DryRunResult {
return Boolean(value && typeof value === 'object' && (value as DryRunResult).dryRun === true)
}
export { TRELLO_API_BASE_URL, isMutatingMethod, mutationPreview, normalizeTrelloPath, trelloUrl }