/* eslint-disable no-await-in-loop */ import { AxiosError, AxiosResponse } from "axios"; import { request } from "@app/lib/config/request"; import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TVercelBranches } from "@app/services/integration-auth/integration-auth-types"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { VercelConnectionMethod } from "./vercel-connection-enums"; import { TVercelConnectionConfig, TVercelConnectionInput, VercelApp, VercelEnvironment, VercelOrgWithApps } from "./vercel-connection-types"; export const getVercelConnectionListItem = () => { return { name: "Vercel" as const, app: AppConnection.Vercel as const, methods: Object.values(VercelConnectionMethod) as [VercelConnectionMethod.ApiToken] }; }; export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => { const { credentials: inputCredentials } = config; let response: AxiosResponse | null = null; try { response = await request.get(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, { headers: { Authorization: `Bearer ${inputCredentials.apiToken}` } }); } catch (error: unknown) { if (error instanceof AxiosError) { throw new BadRequestError({ message: `Failed to validate credentials: ${error.message || "Unknown error"}` }); } throw new BadRequestError({ message: "Unable to validate connection - verify credentials" }); } if (!response?.data) { throw new InternalServerError({ message: "Failed to get organizations: Response was empty" }); } return inputCredentials; }; interface ApiResponse { pagination?: { count: number; next: number; }; data: T[]; [key: string]: unknown; } async function fetchAllPages( apiUrl: string, apiToken: string, initialParams: Record = {}, dataPath?: string ): Promise { const allItems: T[] = []; let hasMoreItems = true; let params: Record = { ...initialParams, limit: 100 }; while (hasMoreItems) { try { const response = await request.get>(apiUrl, { params, headers: { Authorization: `Bearer ${apiToken}`, "Accept-Encoding": "application/json" } }); if (!response?.data) { throw new InternalServerError({ message: `Failed to fetch data from ${apiUrl}: Response was empty or malformed` }); } let itemsData: T[]; if (dataPath && dataPath in response.data) { itemsData = response.data[dataPath] as T[]; } else { itemsData = response.data.data; } if (!Array.isArray(itemsData)) { throw new InternalServerError({ message: `Failed to fetch data from ${apiUrl}: Expected array but got ${typeof itemsData}` }); } allItems.push(...itemsData); if (response.data.pagination?.next) { params = { ...params, since: response.data.pagination.next }; } else { hasMoreItems = false; } } catch (error) { if (error instanceof AxiosError) { throw new BadRequestError({ message: `Failed to fetch data from ${apiUrl}: ${error.message || "Unknown error"}` }); } throw error; } } return allItems; } async function fetchOrgProjects(orgId: string, apiToken: string): Promise { return fetchAllPages( `${IntegrationUrls.VERCEL_API_URL}/v9/projects`, apiToken, { teamId: orgId }, "projects" ); } async function fetchProjectEnvironments(projectId: string, apiToken: string): Promise { return fetchAllPages( `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${projectId}/custom-environments`, apiToken, {}, "environments" ); } async function fetchPreviewBranches(projectId: string, apiToken: string): Promise { const { data } = await request.get( `${IntegrationUrls.VERCEL_API_URL}/v1/integrations/git-branches`, { params: { projectId }, headers: { Authorization: `Bearer ${apiToken}`, "Accept-Encoding": "application/json" } } ); return data.filter((b) => b.ref !== "main").map((b) => b.ref); } type VercelTeam = { id: string; name: string; slug: string; }; type VercelUserResponse = { user: { id: string; name: string; username: string; }; }; export const listProjects = async (appConnection: TVercelConnectionInput): Promise => { const { credentials } = appConnection; const { apiToken } = credentials; const orgs = await fetchAllPages(`${IntegrationUrls.VERCEL_API_URL}/v2/teams`, apiToken, {}, "teams"); const personalAccountResponse = await request.get(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, { headers: { Authorization: `Bearer ${apiToken}`, "Accept-Encoding": "application/json" } }); if (personalAccountResponse?.data?.user) { const { user } = personalAccountResponse.data; orgs.push({ id: user.id, name: user.name || "Personal Account", slug: user.username || "personal" }); } const orgsWithApps: VercelOrgWithApps[] = []; const orgPromises = orgs.map(async (org) => { try { const projects = await fetchOrgProjects(org.id, apiToken); const enhancedProjectsPromises = projects.map(async (project) => { try { const [environments, previewBranches] = await Promise.all([ fetchProjectEnvironments(project.id, apiToken), fetchPreviewBranches(project.id, apiToken) ]); return { name: project.name, id: project.id, envs: environments, previewBranches }; } catch (error) { return { name: project.name, id: project.id, envs: [], previewBranches: [] }; } }); const enhancedProjects = await Promise.all(enhancedProjectsPromises); return { ...org, apps: enhancedProjects }; } catch (error) { return null; } }); const results = await Promise.all(orgPromises); results.forEach((result) => { if (result !== null) { orgsWithApps.push(result); } }); return orgsWithApps; }; export const getProjectEnvironmentVariables = (project: VercelApp): Record => { const envVars: Record = {}; if (!project.envs) return envVars; project.envs.forEach((env) => { if (env.value && env.type !== "gitBranch") { const { key, value } = env; envVars[key] = value; } }); return envVars; };