Skip to content

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

Package listing

@kody/trello

src/client.ts

134 lines · 3.4 KB · TypeScript
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 }