import { kitFetch, resolveKitAuth, type ResolvedKitAuth } from './auth.ts' import type { DryRunResult, JsonRecord, KitAuthInput, KitPageInfo, MutationInput } from './types.ts' import { isMutatingMethod, nextStepForAuth } from './setup.ts' import { kitUrl, mutationPreview, normalizeKitPath, rejectBroadcastSend } from './safety.ts' export class KitApiError 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 = 'KitApiError' this.status = options.status this.body = options.body this.method = options.method this.path = options.path } } export type KitRequestInput = KitAuthInput & MutationInput & { path: string method?: string query?: JsonRecord body?: JsonRecord } export type KitResponse = { data: T pageInfo: KitPageInfo status: number } export { kitUrl, mutationPreview, normalizeKitPath, rejectBroadcastSend } function kitErrorMessage(body: unknown): string | null { if (!body || typeof body !== 'object') return null const record = body as { errors?: unknown; message?: unknown } if (Array.isArray(record.errors) && record.errors.every((item) => typeof item === 'string')) { return record.errors.join('; ') } if (typeof record.message === 'string' && record.message.length > 0) { return record.message } return null } export function pageInfoFrom(body: unknown): KitPageInfo { const pagination = body && typeof body === 'object' ? ((body as { pagination?: { has_next_page?: unknown; end_cursor?: unknown } }).pagination ?? null) : null const after = typeof pagination?.end_cursor === 'string' ? pagination.end_cursor : null return { hasNextPage: Boolean(pagination?.has_next_page && after), after, } } export async function kitRequest(input: KitRequestInput): Promise> { const method = (input.method ?? 'GET').toUpperCase() const path = normalizeKitPath(input.path) if (isMutatingMethod(method)) { const preview = mutationPreview(input, { method, path, body: input.body, }) if (preview) { return { data: preview as T, pageInfo: { hasNextPage: false, after: null }, status: 0, } } } const auth = await resolveKitAuth(input) return kitRequestWithAuth(auth, { method, path, query: input.query, body: input.body, }) } export async function kitRequestWithAuth( auth: ResolvedKitAuth, input: { method: string path: string query?: JsonRecord body?: JsonRecord }, ): Promise> { const url = kitUrl(input.path, input.query) const headers = new Headers({ Accept: 'application/json', 'User-Agent': 'kody-kit/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 kitFetch(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 = kitErrorMessage(parsed) || (typeof parsed === 'string' ? parsed.slice(0, 400) : response.statusText) throw new KitApiError( [ `Kit ${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, pageInfo: pageInfoFrom(parsed), status: response.status, } } export async function kitListAll( input: KitAuthInput & { path: string itemKey: string query?: JsonRecord maxItems?: number }, ): Promise> { const maxItems = input.maxItems ?? 25 const items: Array = [] let after: string | undefined for (;;) { const remaining = maxItems - items.length const perPage = Math.min(100, remaining) const page = await kitRequest({ ...input, method: 'GET', path: input.path, query: { ...(input.query ?? {}), per_page: perPage, ...(after ? { after } : {}), }, }) const chunk = page.data?.[input.itemKey] if (Array.isArray(chunk)) items.push(...(chunk as Array)) if (items.length >= maxItems || !page.pageInfo.hasNextPage || !page.pageInfo.after) { return items.slice(0, maxItems) } after = page.pageInfo.after } } export function unwrapRecord( body: unknown, key: string, action: string, ): T { if (!body || typeof body !== 'object') { throw new Error(`${action}: Kit returned no ${key}.`) } const value = (body as JsonRecord)[key] if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`${action}: Kit returned no ${key}.`) } return value as T } export { isMutatingMethod }