Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 113 additions & 117 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"author": "Supabase",
"license": "MIT",
"dependencies": {
"cross-fetch": "^3.1.5"
"cross-fetch": "^4.0.0"
},
"devDependencies": {
"@types/node": "^18.15.0",
Expand All @@ -43,4 +43,4 @@
"ts-jest": "^29.1.0",
"typescript": "^5.0.4"
}
}
}
81 changes: 81 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Organization } from './lib/organization'
import { Secret } from './lib/secret'
import { Volume } from './lib/volume'
import { Regions } from './lib/regions'
import { APIResponse } from './lib/utils'

export const FLY_API_GRAPHQL = 'https://api.fly.io'
export const FLY_API_HOSTNAME = 'https://api.machines.dev'
Expand Down Expand Up @@ -92,6 +93,47 @@ class Client {
return data
}

// TODO: make sure these methods using this method are using return values correctly
async safeGqlPost<U, V>(payload: GraphQLRequest<U>): Promise<APIResponse<V>> {
try {
const token = this.apiKey
const resp = await crossFetch(`${this.graphqlUrl}/graphql`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!resp.ok) {
const text = await resp.text()
return {
data: undefined,
error: { status: resp.status, message: text },
}
}
const { data, errors }: GraphQLResponse<V> = await resp.json()
if (errors) {
return {
data: undefined,
error: { status: 500, message: JSON.stringify(errors) },
}
}
return { data, error: undefined }
} catch (e) {
if (e instanceof Error) {
return { data: undefined, error: { status: 500, message: e.message } }
}
if (typeof e === 'string') {
return { data: undefined, error: { status: 500, message: e } }
}
return {
data: undefined,
error: { status: 500, message: 'An unknown error occurred.' },
}
}
}

async restOrThrow<U, V>(
path: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
Expand All @@ -111,6 +153,45 @@ class Client {
}
return text ? JSON.parse(text) : undefined
}

async safeRest<U, V>(
path: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
body?: U,
headers?: Record<string, string>
): Promise<APIResponse<V>> {
try {
const resp = await crossFetch(`${this.apiUrl}/v1/${path}`, {
method,
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify(body),
})
if (!resp.ok) {
const text = await resp.text()
return {
data: undefined,
error: { status: resp.status, message: text },
}
}
const data = await resp.json()
return { data, error: undefined }
} catch (e) {
if (e instanceof Error) {
return { data: undefined, error: { status: 500, message: e.message } }
}
if (typeof e === 'string') {
return { data: undefined, error: { status: 500, message: e } }
}
return {
data: undefined,
error: { status: 500, message: 'An unknown error occurred.' },
}
}
}
}

export default Client
134 changes: 118 additions & 16 deletions src/lib/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Client from '../client'
import { APIResponse } from './utils'

export type ListAppRequest = string

Expand All @@ -11,9 +12,43 @@ export interface ListAppResponse {
}[]
}

export interface ListAppDetailedResponse {
apps: AppDetailedResponse[]
}

export type GetAppRequest = string

const getAppQuery = `query($name: String!) {
const getOrganizationAppsDetailedQuery = `query($slug: String!) {
organization(slug: $slug) {
apps {
nodes {
name
status
organization {
name
slug
}
ipAddresses {
nodes {
type
region
address
}
}
machines {
nodes {
id
name
state
region
}
}
}
}
}
}`

const getAppDetailedQuery = `query($name: String!) {
app(name: $name) {
name
status
Expand All @@ -28,6 +63,14 @@ const getAppQuery = `query($name: String!) {
address
}
}
machines {
nodes {
id
name
state
region
}
}
}
}`

Expand All @@ -44,7 +87,18 @@ export interface AppResponse {
name: string
slug: string
}
}

export interface AppDetailedResponse extends AppResponse {
ipAddresses: IPAddress[]
machines: AppMachine[]
}

export interface AppMachine {
id: string
name: string
state: string
region: string
}

export interface IPAddress {
Expand All @@ -67,37 +121,85 @@ export class App {
this.client = client
}

async listApps(org_slug: ListAppRequest): Promise<ListAppResponse> {
async listApps(
org_slug: ListAppRequest
): Promise<APIResponse<ListAppResponse>> {
const path = `apps?org_slug=${org_slug}`
return await this.client.restOrThrow(path)
return await this.client.safeRest(path)
}

async getApp(app_name: GetAppRequest): Promise<AppResponse> {
async listAppsDetailed(
org_slug: ListAppRequest
): Promise<APIResponse<ListAppDetailedResponse>> {
const response = await this.client.safeGqlPost<
string,
{ organization: any }
>({
query: getOrganizationAppsDetailedQuery,
variables: { slug: org_slug },
})
if (response.error) {
return response
}
return {
data: parseOrgResponse(response.data.organization).apps,
error: undefined,
}
}

async getApp(app_name: GetAppRequest): Promise<APIResponse<AppResponse>> {
const path = `apps/${app_name}`
return await this.client.restOrThrow(path)
return await this.client.safeRest(path)
}

async getAppDetailed(app_name: GetAppRequest): Promise<AppResponse> {
const { app } = await this.client.gqlPostOrThrow({
query: getAppQuery,
async getAppDetailed(
app_name: GetAppRequest
): Promise<APIResponse<AppDetailedResponse>> {
const response = await this.client.safeGqlPost<string, { app: any }>({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid typing app: any in calls to safeGqlPost? Always define the interface types returned from graphql api call.

query: getAppDetailedQuery,
variables: { name: app_name },
}) as { app: AppResponse }
})

const ipAddresses = app.ipAddresses as unknown as { nodes: IPAddress[] }
if (response.error) {
return response
}

return {
...app,
ipAddresses: ipAddresses.nodes,
data: parseAppResponse(response.data.app),
error: undefined,
}
}

async createApp(payload: CreateAppRequest): Promise<void> {
async createApp(payload: CreateAppRequest): Promise<APIResponse<void>> {
const path = 'apps'
return await this.client.restOrThrow(path, 'POST', payload)
return await this.client.safeRest(path, 'POST', payload)
}

async deleteApp(app_name: DeleteAppRequest): Promise<void> {
async deleteApp(app_name: DeleteAppRequest): Promise<APIResponse<void>> {
const path = `apps/${app_name}`
return await this.client.restOrThrow(path, 'DELETE')
return await this.client.safeRest(path, 'DELETE')
}
}

function parseAppResponse(appData: any) {
const ipAddresses = parseNodes<IPAddress>(appData, 'ipAddresses')
const machines = parseNodes<AppMachine>(appData, 'machines')

return {
...appData,
ipAddresses,
machines,
}
}

function parseOrgResponse(orgData: any) {
const apps = parseNodes(orgData, 'apps').map(parseAppResponse)
return {
...orgData,
apps,
}
}

function parseNodes<T>(data: any, key: string): T[] {
return data[key].nodes
}
Loading
Loading