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<T> = {
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<T = unknown>(input: KitRequestInput): Promise<KitResponse<T>> {
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<T>(auth, {
method,
path,
query: input.query,
body: input.body,
})
}
export async function kitRequestWithAuth<T = unknown>(
auth: ResolvedKitAuth,
input: {
method: string
path: string
query?: JsonRecord
body?: JsonRecord
},
): Promise<KitResponse<T>> {
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<T>(
input: KitAuthInput & {
path: string
itemKey: string
query?: JsonRecord
maxItems?: number
},
): Promise<Array<T>> {
const maxItems = input.maxItems ?? 25
const items: Array<T> = []
let after: string | undefined
for (;;) {
const remaining = maxItems - items.length
const perPage = Math.min(100, remaining)
const page = await kitRequest<JsonRecord>({
...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<T>))
if (items.length >= maxItems || !page.pageInfo.hasNextPage || !page.pageInfo.after) {
return items.slice(0, maxItems)
}
after = page.pageInfo.after
}
}
export function unwrapRecord<T extends JsonRecord>(
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 }