Skip to content

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

Package listing

@kody/sentry

src/openapi-client.ts

143 lines · 5.2 KB · TypeScript
/**
 * Scaffolded Sentry OpenAPI client (dependency-free ESM).
 *
 * Source: openapi_client_scaffold against
 * https://raw.githubusercontent.com/getsentry/sentry-api-schema/main/openapi-derefed.json
 * Slugs: listorganizations, listorganizationprojects, listorganizationissues
 * Auth: bearerSecret sentryAuthToken
 *
 * Host defaults to sentry.io. Pass `host` for us.sentry.io, de.sentry.io,
 * or a self-hosted instance. Secret placeholders only resolve for approved
 * hosts.
 */

import { authorizationHeader, getSentryFetch, resolveIntegrationName, type SentryAuthSelection } from './auth.ts'
import { sentryApiBaseUrl } from './host.ts'

export type QueryValue = string | number | boolean | null | undefined
export type QueryInput = Record<string, QueryValue | QueryValue[]>

function authHeaders(auth: SentryAuthSelection = {}): Record<string, string> {
	if (resolveIntegrationName(auth)) return {}
	return { Authorization: authorizationHeader(auth) }
}

export function buildUrl(
	pathTemplate: string,
	params: Record<string, unknown> = {},
	host?: unknown,
): string {
	return sentryApiBaseUrl(host) + pathTemplate.replace(/\{([^}]+)\}/g, (_match, name: string) => {
		const value = params[name]
		if (value === undefined || value === null) {
			throw new Error(`Missing required path parameter: ${name}`)
		}
		return encodeURIComponent(String(value))
	})
}

export function appendQuery(url: string, query: QueryInput = {}): string {
	const search = new URLSearchParams()
	for (const [key, value] of Object.entries(query)) {
		if (value === undefined || value === null || value === '') continue
		if (Array.isArray(value)) {
			for (const item of value) {
				if (item === undefined || item === null || item === '') continue
				search.append(key, String(item))
			}
			continue
		}
		search.append(key, String(value))
	}
	const qs = search.toString()
	return qs ? `${url}?${qs}` : url
}

function mergeHeaders(
	userHeaders: Record<string, string> | undefined,
	authHeaders: Record<string, string>,
): Record<string, string> {
	const merged: Record<string, string> = { ...(userHeaders ?? {}) }
	for (const [key, value] of Object.entries(authHeaders)) {
		const lower = key.toLowerCase()
		for (const existing of Object.keys(merged)) {
			if (existing.toLowerCase() === lower) delete merged[existing]
		}
		merged[key] = value
	}
	return merged
}

export type ScaffoldInput = SentryAuthSelection & {
	params?: Record<string, unknown>
	query?: QueryInput
	headers?: Record<string, string>
	body?: unknown
	host?: string
}

function requestAuth(input: SentryAuthSelection = {}): SentryAuthSelection {
	return { secretName: input.secretName, integration: input.integration }
}

async function sentryFetch(url: string, init: RequestInit, auth: SentryAuthSelection = {}): Promise<Response> {
	const fetchFn = await getSentryFetch(auth)
	return fetchFn(url, init)
}

/** GET /api/0/organizations/ — List Your Organizations */
export async function listorganizations(input: ScaffoldInput = {}): Promise<Response> {
	const params = input.params ?? {}
	const url = appendQuery(buildUrl('/api/0/organizations/', params, input.host), input.query)
	const auth = requestAuth(input)
	return sentryFetch(url, { method: 'GET', headers: mergeHeaders(input.headers, authHeaders(auth)) }, auth)
}

/** GET /api/0/organizations/{organization_id_or_slug}/projects/ */
export async function listorganizationprojects(input: ScaffoldInput = {}): Promise<Response> {
	const params = input.params ?? {}
	if (params.organization_id_or_slug === undefined || params.organization_id_or_slug === null) {
		throw new Error('Missing required path parameter: organization_id_or_slug')
	}
	const url = appendQuery(
		buildUrl('/api/0/organizations/{organization_id_or_slug}/projects/', params, input.host),
		input.query,
	)
	const auth = requestAuth(input)
	return sentryFetch(url, { method: 'GET', headers: mergeHeaders(input.headers, authHeaders(auth)) }, auth)
}

/** GET /api/0/organizations/{organization_id_or_slug}/issues/ */
export async function listorganizationissues(input: ScaffoldInput = {}): Promise<Response> {
	const params = input.params ?? {}
	if (params.organization_id_or_slug === undefined || params.organization_id_or_slug === null) {
		throw new Error('Missing required path parameter: organization_id_or_slug')
	}
	const url = appendQuery(
		buildUrl('/api/0/organizations/{organization_id_or_slug}/issues/', params, input.host),
		input.query,
	)
	const auth = requestAuth(input)
	return sentryFetch(url, { method: 'GET', headers: mergeHeaders(input.headers, authHeaders(auth)) }, auth)
}

/**
 * Minimal fetch for endpoints absent from the public OpenAPI inventory
 * (e.g. /api/0/issues/{id}/events/latest/). Path is relative to /api/0.
 */
export async function rawSentryRequest(
	path: string,
	options: { query?: QueryInput; init?: RequestInit; host?: string } & SentryAuthSelection = {},
): Promise<Response> {
	const normalizedPath = path.startsWith('/') ? path : '/' + path
	const url = appendQuery(`${sentryApiBaseUrl(options.host)}/api/0${normalizedPath}`, options.query)
	const auth = requestAuth(options)
	const headers = mergeHeaders(
		{
			accept: 'application/json',
			...(options.init?.headers as Record<string, string> | undefined),
		},
		authHeaders(auth),
	)
	return sentryFetch(url, { ...options.init, headers }, auth)
}