import {
appsList,
appsShow,
currenttokenShow,
machinesList,
machinesOrgList,
machinesShow,
} from './lib/openapi-client.ts'
const flyApiToken = '{{secret:flyApiToken}}'
const flyGraphqlUrl = 'https://api.fly.io/graphql'
const flyApiUrl = 'https://api.fly.io'
export function clean(value) {
return String(value ?? '').trim()
}
export function positiveInt(value, fallback, max = 100) {
const number = Math.floor(Number(value))
if (!Number.isFinite(number) || number <= 0) return fallback
return Math.min(number, max)
}
export async function parseJsonResponse(response, label = 'Fly API') {
const text = await response.text()
let body = null
try {
body = text ? JSON.parse(text) : null
} catch {
body = { raw: text }
}
if (!response.ok) {
throw new Error(label + ' HTTP ' + response.status + ': ' + JSON.stringify(body).slice(0, 800))
}
return body
}
export async function requestGraphql({ query, variables = {} } = {}) {
if (!clean(query)) throw new Error('query is required.')
const response = await fetch(flyGraphqlUrl, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + flyApiToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
})
const body = await parseJsonResponse(response, 'Fly GraphQL')
if (Array.isArray(body?.errors) && body.errors.length > 0) {
const message = body.errors.map((error) => error.message || JSON.stringify(error)).join('; ')
throw new Error('Fly GraphQL error: ' + message)
}
return body?.data ?? body
}
/** Prometheus / app-logs helper against api.fly.io (not Machines OpenAPI). */
export async function requestFlyApi(path, { method = 'GET', search = {}, headers = {}, body } = {}) {
const url = new URL(path, flyApiUrl)
for (const [key, value] of Object.entries(search)) {
if (value !== undefined && value !== null && clean(value)) {
url.searchParams.set(key, String(value))
}
}
const response = await fetch(url, {
method,
headers: {
Authorization: 'FlyV1 ' + flyApiToken,
Accept: 'application/json',
...headers,
},
body,
})
return await parseJsonResponse(response, 'Fly API')
}
export function pageInfo(connection) {
return connection && typeof connection === 'object' ? connection.pageInfo || {} : {}
}
export function nodes(connection) {
return Array.isArray(connection?.nodes) ? connection.nodes : []
}
export async function collectConnection(fetchPage, { maxPages = 20 } = {}) {
const all = []
let after = null
for (let page = 0; page < positiveInt(maxPages, 20, 100); page += 1) {
const connection = await fetchPage(after)
all.push(...nodes(connection))
const info = pageInfo(connection)
if (!info.hasNextPage || !info.endCursor) break
after = info.endCursor
}
return all
}
export function resolveOrgSlug(params = {}) {
return (
clean(params.organizationSlug) ||
clean(params.orgSlug) ||
clean(params.slug) ||
clean(params.org_slug) ||
'personal'
)
}
export function summarizeMachine(machine, appHint = null) {
const appName = machine?.app_name || machine?.app?.name || appHint?.name || null
const appId = machine?.app?.id || appHint?.id || null
return {
id: machine?.id ?? null,
name: machine?.name ?? null,
state: machine?.state ?? null,
region: machine?.region ?? null,
app: appName || appId ? { id: appId, name: appName } : null,
image: machine?.config?.image ?? machine?.image ?? null,
createdAt: machine?.created_at ?? machine?.createdAt ?? null,
updatedAt: machine?.updated_at ?? machine?.updatedAt ?? null,
}
}
export function summarizeApp(app) {
return {
id: app?.id ?? null,
name: app?.name ?? null,
status: app?.status ?? null,
deployed: app?.status === 'deployed' ? true : app?.deployed ?? null,
organization: app?.organization
? {
id: app.organization.id ?? null,
slug: app.organization.slug ?? null,
name: app.organization.name ?? null,
}
: null,
machineCount: app?.machine_count ?? null,
volumeCount: app?.volume_count ?? null,
}
}
export async function machinesAppsList(params = {}) {
const orgSlug = resolveOrgSlug(params)
const query = { org_slug: orgSlug }
if (clean(params.appRole) || clean(params.app_role)) {
query.app_role = clean(params.appRole || params.app_role)
}
const response = await appsList({ query })
const body = await parseJsonResponse(response, 'Fly Machines apps_list')
const apps = Array.isArray(body?.apps) ? body.apps : Array.isArray(body) ? body : []
return {
organization: { slug: orgSlug },
totalApps: body?.total_apps ?? apps.length,
apps,
}
}
export async function machinesAppShow(appName) {
const name = clean(appName)
if (!name) throw new Error('appName is required.')
const response = await appsShow({ params: { app_name: name } })
return await parseJsonResponse(response, 'Fly Machines apps_show')
}
export async function machinesListForApp(appName, params = {}) {
const name = clean(appName)
if (!name) throw new Error('appName is required.')
const query = {}
if (clean(params.state)) query.state = clean(params.state)
if (clean(params.region)) query.region = clean(params.region)
if (params.includeDeleted === true || params.include_deleted === true) query.include_deleted = true
if (params.summary !== false) query.summary = true
const response = await machinesList({
params: { app_name: name },
query,
})
const body = await parseJsonResponse(response, 'Fly Machines machines_list')
return Array.isArray(body) ? body : Array.isArray(body?.machines) ? body.machines : []
}
export async function machinesGet(appName, machineId) {
const name = clean(appName)
const id = clean(machineId)
if (!name) throw new Error('appName is required.')
if (!id) throw new Error('machineId is required.')
const response = await machinesShow({
params: { app_name: name, machine_id: id },
})
return await parseJsonResponse(response, 'Fly Machines machines_show')
}
export async function machinesListForOrg(params = {}) {
const orgSlug = resolveOrgSlug(params)
const query = {}
if (clean(params.state)) query.state = clean(params.state)
if (clean(params.region)) query.region = clean(params.region)
if (params.includeDeleted === true || params.include_deleted === true) query.include_deleted = true
if (params.summary !== false) query.summary = true
if (clean(params.updatedAfter) || clean(params.updated_after)) {
query.updated_after = clean(params.updatedAfter || params.updated_after)
}
// Org machines endpoint returns one page; optional cursor continues when present.
const response = await machinesOrgList({
params: { org_slug: orgSlug },
query: {
...query,
...(clean(params.cursor) ? { cursor: clean(params.cursor) } : {}),
},
})
const body = await parseJsonResponse(response, 'Fly Machines machines_org_list')
const machines = Array.isArray(body?.machines)
? body.machines
: Array.isArray(body)
? body
: []
return {
organization: { slug: orgSlug },
machines,
lastMachineId: body?.last_machine_id ?? null,
lastUpdatedAt: body?.last_updated_at ?? null,
}
}
export async function machinesCurrentToken() {
const response = await currenttokenShow()
return await parseJsonResponse(response, 'Fly Machines currenttoken_show')
}