← Public packages
@kody/openapi
Bind an OpenAPI spec and call selected operations with saved integration or secret names.
src/request.ts
176 lines · 4.4 KB · TypeScriptimport {
createAuthenticatedFetch,
secretHeaders,
} from 'kody:runtime'
import { type OpenApiBoundAuth } from './auth.ts'
import {
BoundedBodyTooLargeError,
readBoundedBody,
} from './bounded-body.ts'
import { type OpenApiBinding, type OpenApiBindingOperation } from './selection.ts'
import { assertPinnedToApiBaseUrl, buildOperationUrl } from './url.ts'
export const openApiOperationResponseMaxBytes = 300_000
export type OpenApiOperationRequestArgs = {
params?: Record<string, unknown>
query?: Record<string, unknown>
headers?: Record<string, unknown>
body?: unknown
}
export type OpenApiOperationRequestResult = {
status: number
ok: boolean
contentType: string | null
body: unknown
truncated: boolean
}
function asStringRecord(
value: Record<string, unknown> | undefined,
label: string,
): Record<string, string> {
if (value == null) return {}
const result: Record<string, string> = {}
for (const [key, entry] of Object.entries(value)) {
if (entry == null) continue
if (typeof entry === 'object') {
throw new Error(`${label}.${key} must be a string or number`)
}
result[key] = String(entry)
}
return result
}
function shouldSendBody(operation: OpenApiBindingOperation) {
return !['get', 'head'].includes(operation.method)
}
function serializeBody(
body: unknown,
operation: OpenApiBindingOperation,
): string {
const contentType = operation.requestBody?.contentType ?? 'application/json'
if (contentType.includes('json') || typeof body === 'object') {
return JSON.stringify(body)
}
return String(body)
}
function authHeaders(auth: OpenApiBoundAuth): Record<string, string> {
switch (auth.kind) {
case 'none':
case 'integration':
return {}
case 'bearerSecret':
return {
Authorization: `Bearer {{secret:${auth.secretName}}}`,
}
case 'headerSecret':
return {
[auth.headerName]: `{{secret:${auth.secretName}}}`,
}
case 'basicSecrets':
return {
Authorization: secretHeaders.basic({
usernameSecret: auth.usernameSecret,
passwordSecret: auth.passwordSecret,
}),
}
default: {
const _exhaustive: never = auth
return _exhaustive
}
}
}
async function resolveFetch(
auth: OpenApiBoundAuth,
fetchImpl?: typeof fetch,
): Promise<typeof fetch> {
if (fetchImpl) return fetchImpl
if (auth.kind === 'integration') {
return createAuthenticatedFetch(auth.provider)
}
return fetch
}
async function readOperationResponse(
response: Response,
): Promise<OpenApiOperationRequestResult> {
const contentType = response.headers.get('content-type')
let raw: string
let truncated = false
try {
raw = await readBoundedBody(response, openApiOperationResponseMaxBytes)
} catch (error) {
if (error instanceof BoundedBodyTooLargeError) {
truncated = true
raw = await response.text().catch(() => '')
raw = raw.slice(0, openApiOperationResponseMaxBytes)
} else {
throw error
}
}
let body: unknown = raw
if (contentType?.includes('json') && raw.length > 0) {
try {
body = JSON.parse(raw)
} catch {
body = raw
}
}
return {
status: response.status,
ok: response.ok,
contentType,
body,
truncated,
}
}
export async function executeOpenApiOperationRequest(input: {
binding: OpenApiBinding
operation: OpenApiBindingOperation
args?: OpenApiOperationRequestArgs
fetchImpl?: typeof fetch
}): Promise<OpenApiOperationRequestResult> {
const args = input.args ?? {}
const params = asStringRecord(args.params, 'params')
const userHeaders = asStringRecord(args.headers, 'headers')
const url = buildOperationUrl({
apiBaseUrl: input.binding.apiBaseUrl,
path: input.operation.path,
params,
query: args.query ?? {},
})
assertPinnedToApiBaseUrl(url, input.binding.apiBaseUrl)
const headers: Record<string, string> = {
...authHeaders(input.binding.auth),
...userHeaders,
}
if (
args.body !== undefined &&
shouldSendBody(input.operation) &&
!Object.keys(headers).some((key) => key.toLowerCase() === 'content-type')
) {
headers['Content-Type'] =
input.operation.requestBody?.contentType ?? 'application/json'
}
const requestInit: RequestInit = {
method: input.operation.method.toUpperCase(),
headers,
}
if (args.body !== undefined && shouldSendBody(input.operation)) {
requestInit.body = serializeBody(args.body, input.operation)
}
const fetchImpl = await resolveFetch(input.binding.auth, input.fetchImpl)
const response = await fetchImpl(url.toString(), requestInit)
return readOperationResponse(response)
}