744 lines
25 KiB
TypeScript
744 lines
25 KiB
TypeScript
import { type PrismaClient } from "@trigger.dev/database";
|
|
import { type Result, fromPromise, ok, okAsync, ResultAsync } from "neverthrow";
|
|
import { env } from "~/env.server";
|
|
import { logger } from "~/services/logger.server";
|
|
import { OrgIntegrationRepository } from "~/models/orgIntegration.server";
|
|
import type {
|
|
VercelCustomEnvironment,
|
|
VercelEnvironmentVariable,
|
|
} from "~/models/vercelIntegration.server";
|
|
import { VercelIntegrationRepository } from "~/models/vercelIntegration.server";
|
|
import { type GitHubAppInstallation } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github";
|
|
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
|
|
import { isReservedForExternalSync } from "~/v3/environmentVariableRules.server";
|
|
import type { VercelProjectIntegrationData } from "~/v3/vercel/vercelProjectIntegrationSchema";
|
|
import { VercelProjectIntegrationDataSchema } from "~/v3/vercel/vercelProjectIntegrationSchema";
|
|
import { BasePresenter } from "./basePresenter.server";
|
|
|
|
type VercelSettingsOptions = {
|
|
projectId: string;
|
|
organizationId: string;
|
|
};
|
|
|
|
export type VercelSettingsResult = {
|
|
enabled: boolean;
|
|
hasOrgIntegration: boolean;
|
|
authInvalid?: boolean;
|
|
authError?: string;
|
|
connectedProject?: {
|
|
id: string;
|
|
vercelProjectId: string;
|
|
vercelProjectName: string;
|
|
vercelTeamId: string | null;
|
|
integrationData: VercelProjectIntegrationData;
|
|
createdAt: Date;
|
|
};
|
|
isGitHubConnected: boolean;
|
|
hasStagingEnvironment: boolean;
|
|
hasPreviewEnvironment: boolean;
|
|
customEnvironments: VercelCustomEnvironment[];
|
|
/** Whether autoAssignCustomDomains is enabled on the Vercel project. null if unknown. */
|
|
autoAssignCustomDomains?: boolean | null;
|
|
/** URL to manage Vercel integration access (project sharing) on vercel.com */
|
|
vercelManageAccessUrl?: string;
|
|
/** The currently pinned TRIGGER_VERSION on Vercel production, if set. Used to surface
|
|
* the pin in the UI and prompt the user to clear it when atomic deployments are disabled. */
|
|
currentTriggerVersion?: string | null;
|
|
/** True when the Vercel lookup for TRIGGER_VERSION failed (network/auth/etc). Distinct
|
|
* from "no pin set" — the UI uses this to warn the user and still prompt them on disable
|
|
* so they can manually verify that production isn't pinned. */
|
|
currentTriggerVersionFetchFailed?: boolean;
|
|
};
|
|
|
|
export type VercelAvailableProject = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
export type VercelOnboardingData = {
|
|
customEnvironments: VercelCustomEnvironment[];
|
|
environmentVariables: VercelEnvironmentVariable[];
|
|
availableProjects: VercelAvailableProject[];
|
|
hasProjectSelected: boolean;
|
|
authInvalid?: boolean;
|
|
authError?: string;
|
|
existingVariables: Record<string, { environments: string[] }>; // Environment slugs (non-archived only)
|
|
gitHubAppInstallations: GitHubAppInstallation[];
|
|
isGitHubConnected: boolean;
|
|
isOnboardingComplete: boolean;
|
|
};
|
|
|
|
export class VercelSettingsPresenter extends BasePresenter {
|
|
/**
|
|
* Get Vercel integration settings for the settings page
|
|
*/
|
|
public async call({
|
|
projectId,
|
|
organizationId,
|
|
}: VercelSettingsOptions): Promise<Result<VercelSettingsResult, unknown>> {
|
|
const vercelIntegrationEnabled = OrgIntegrationRepository.isVercelSupported;
|
|
|
|
if (!vercelIntegrationEnabled) {
|
|
return ok({
|
|
enabled: false,
|
|
hasOrgIntegration: false,
|
|
authInvalid: false,
|
|
connectedProject: undefined,
|
|
isGitHubConnected: false,
|
|
hasStagingEnvironment: false,
|
|
hasPreviewEnvironment: false,
|
|
customEnvironments: [],
|
|
} as VercelSettingsResult);
|
|
}
|
|
|
|
const orgIntegrationResult = await fromPromise(
|
|
(this._replica as PrismaClient).organizationIntegration.findFirst({
|
|
where: {
|
|
organizationId,
|
|
service: "VERCEL",
|
|
deletedAt: null,
|
|
},
|
|
include: {
|
|
tokenReference: true,
|
|
},
|
|
}),
|
|
(error) => error
|
|
);
|
|
|
|
if (orgIntegrationResult.isErr()) {
|
|
logger.error("Unexpected error in VercelSettingsPresenter.call", {
|
|
error: orgIntegrationResult.error,
|
|
});
|
|
return ok({
|
|
enabled: true,
|
|
hasOrgIntegration: false,
|
|
authInvalid: true,
|
|
authError:
|
|
orgIntegrationResult.error instanceof Error
|
|
? orgIntegrationResult.error.message
|
|
: "Failed to fetch organization integration",
|
|
connectedProject: undefined,
|
|
isGitHubConnected: false,
|
|
hasStagingEnvironment: false,
|
|
hasPreviewEnvironment: false,
|
|
customEnvironments: [],
|
|
} as VercelSettingsResult);
|
|
}
|
|
|
|
const orgIntegration = orgIntegrationResult.value;
|
|
const hasOrgIntegration = orgIntegration !== null;
|
|
|
|
if (hasOrgIntegration) {
|
|
const tokenResult = await VercelIntegrationRepository.validateVercelToken(orgIntegration);
|
|
if (tokenResult.isErr() || !tokenResult.value.isValid) {
|
|
return ok({
|
|
enabled: true,
|
|
hasOrgIntegration: true,
|
|
authInvalid: true,
|
|
authError: tokenResult.isErr() ? tokenResult.error.message : "Vercel token is invalid",
|
|
connectedProject: undefined,
|
|
isGitHubConnected: false,
|
|
hasStagingEnvironment: false,
|
|
hasPreviewEnvironment: false,
|
|
customEnvironments: [],
|
|
} as VercelSettingsResult);
|
|
}
|
|
}
|
|
|
|
const checkOrgIntegration = () =>
|
|
fromPromise(Promise.resolve(hasOrgIntegration), (error) => ({
|
|
type: "other" as const,
|
|
cause: error,
|
|
}));
|
|
|
|
const checkGitHubConnection = () =>
|
|
fromPromise(
|
|
(this._replica as PrismaClient).connectedGithubRepository.findFirst({
|
|
where: {
|
|
projectId,
|
|
repository: {
|
|
installation: {
|
|
deletedAt: null,
|
|
suspendedAt: null,
|
|
},
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
},
|
|
}),
|
|
(error) => ({
|
|
type: "other" as const,
|
|
cause: error,
|
|
})
|
|
).map((repo) => repo !== null);
|
|
|
|
const checkStagingEnvironment = () =>
|
|
fromPromise(
|
|
(this._replica as PrismaClient).runtimeEnvironment.findFirst({
|
|
select: {
|
|
id: true,
|
|
},
|
|
where: {
|
|
projectId,
|
|
type: "STAGING",
|
|
},
|
|
}),
|
|
(error) => ({
|
|
type: "other" as const,
|
|
cause: error,
|
|
})
|
|
).map((env) => env !== null);
|
|
|
|
const checkPreviewEnvironment = () =>
|
|
fromPromise(
|
|
(this._replica as PrismaClient).runtimeEnvironment.findFirst({
|
|
select: {
|
|
id: true,
|
|
},
|
|
where: {
|
|
projectId,
|
|
type: "PREVIEW",
|
|
},
|
|
}),
|
|
(error) => ({
|
|
type: "other" as const,
|
|
cause: error,
|
|
})
|
|
).map((env) => env !== null);
|
|
|
|
const getVercelProjectIntegration = () =>
|
|
fromPromise(
|
|
(this._replica as PrismaClient).organizationProjectIntegration.findFirst({
|
|
where: {
|
|
projectId,
|
|
deletedAt: null,
|
|
organizationIntegration: {
|
|
service: "VERCEL",
|
|
deletedAt: null,
|
|
},
|
|
},
|
|
include: {
|
|
organizationIntegration: true,
|
|
},
|
|
}),
|
|
(error) => ({
|
|
type: "other" as const,
|
|
cause: error,
|
|
})
|
|
).map((integration) => {
|
|
if (!integration) {
|
|
return undefined;
|
|
}
|
|
|
|
const parsedData = VercelProjectIntegrationDataSchema.safeParse(
|
|
integration.integrationData
|
|
);
|
|
|
|
if (!parsedData.success) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
id: integration.id,
|
|
vercelProjectId: integration.externalEntityId,
|
|
vercelProjectName: parsedData.data.vercelProjectName,
|
|
vercelTeamId: parsedData.data.vercelTeamId,
|
|
integrationData: parsedData.data,
|
|
createdAt: integration.createdAt,
|
|
};
|
|
});
|
|
|
|
return ResultAsync.combine([
|
|
checkOrgIntegration(),
|
|
checkGitHubConnection(),
|
|
checkStagingEnvironment(),
|
|
checkPreviewEnvironment(),
|
|
getVercelProjectIntegration(),
|
|
])
|
|
.andThen(
|
|
([
|
|
hasOrgIntegration,
|
|
isGitHubConnected,
|
|
hasStagingEnvironment,
|
|
hasPreviewEnvironment,
|
|
connectedProject,
|
|
]) => {
|
|
const fetchVercelData = async (): Promise<{
|
|
customEnvironments: VercelCustomEnvironment[];
|
|
autoAssignCustomDomains: boolean | null;
|
|
vercelManageAccessUrl?: string;
|
|
currentTriggerVersion: string | null;
|
|
currentTriggerVersionFetchFailed: boolean;
|
|
}> => {
|
|
if (!orgIntegration) {
|
|
return {
|
|
customEnvironments: [],
|
|
autoAssignCustomDomains: null,
|
|
currentTriggerVersion: null,
|
|
currentTriggerVersionFetchFailed: false,
|
|
};
|
|
}
|
|
const clientResult = await VercelIntegrationRepository.getVercelClient(orgIntegration);
|
|
if (clientResult.isErr()) {
|
|
// We couldn't even build a Vercel client — treat as fetch failure so the UI
|
|
// still prompts the user when they disable atomic deployments.
|
|
return {
|
|
customEnvironments: [],
|
|
autoAssignCustomDomains: null,
|
|
currentTriggerVersion: null,
|
|
currentTriggerVersionFetchFailed: true,
|
|
};
|
|
}
|
|
const client = clientResult.value;
|
|
const teamId =
|
|
await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);
|
|
|
|
// Build manage access URL
|
|
let vercelManageAccessUrl: string | undefined;
|
|
const appSlug = env.VERCEL_INTEGRATION_APP_SLUG;
|
|
const integrationData = orgIntegration.integrationData as Record<
|
|
string,
|
|
unknown
|
|
> | null;
|
|
const installationId =
|
|
typeof integrationData?.installationId === "string"
|
|
? integrationData.installationId
|
|
: undefined;
|
|
if (appSlug && installationId && teamId) {
|
|
const teamSlugResult = await VercelIntegrationRepository.getTeamSlug(client, teamId);
|
|
if (teamSlugResult.isOk()) {
|
|
vercelManageAccessUrl = `https://vercel.com/${teamSlugResult.value}/~/integrations/${appSlug}/${installationId}`;
|
|
}
|
|
}
|
|
|
|
if (!connectedProject) {
|
|
return {
|
|
customEnvironments: [],
|
|
autoAssignCustomDomains: null,
|
|
vercelManageAccessUrl,
|
|
currentTriggerVersion: null,
|
|
currentTriggerVersionFetchFailed: false,
|
|
};
|
|
}
|
|
|
|
const [customEnvsResult, autoAssignResult, triggerVersionResult] = await Promise.all([
|
|
VercelIntegrationRepository.getVercelCustomEnvironments(
|
|
client,
|
|
connectedProject.vercelProjectId,
|
|
teamId
|
|
),
|
|
VercelIntegrationRepository.getAutoAssignCustomDomains(
|
|
client,
|
|
connectedProject.vercelProjectId,
|
|
teamId
|
|
),
|
|
VercelIntegrationRepository.getVercelEnvironmentVariableValues(
|
|
client,
|
|
connectedProject.vercelProjectId,
|
|
teamId,
|
|
"production",
|
|
(key) => key === "TRIGGER_VERSION"
|
|
),
|
|
]);
|
|
|
|
let currentTriggerVersion: string | null = null;
|
|
let currentTriggerVersionFetchFailed = false;
|
|
if (triggerVersionResult.isOk()) {
|
|
const match = triggerVersionResult.value.find(
|
|
(envVar) => envVar.key === "TRIGGER_VERSION" && envVar.target.includes("production")
|
|
);
|
|
currentTriggerVersion = match?.value ?? null;
|
|
} else {
|
|
currentTriggerVersionFetchFailed = true;
|
|
logger.warn(
|
|
"Failed to fetch current TRIGGER_VERSION from Vercel — surfacing as unknown",
|
|
{
|
|
projectId,
|
|
vercelProjectId: connectedProject.vercelProjectId,
|
|
error: triggerVersionResult.error.message,
|
|
}
|
|
);
|
|
}
|
|
|
|
return {
|
|
customEnvironments: customEnvsResult.isOk() ? customEnvsResult.value : [],
|
|
autoAssignCustomDomains: autoAssignResult.isOk() ? autoAssignResult.value : null,
|
|
vercelManageAccessUrl,
|
|
currentTriggerVersion,
|
|
currentTriggerVersionFetchFailed,
|
|
};
|
|
};
|
|
|
|
return fromPromise(fetchVercelData(), (error) => ({
|
|
type: "other" as const,
|
|
cause: error,
|
|
})).map(
|
|
({
|
|
customEnvironments,
|
|
autoAssignCustomDomains,
|
|
vercelManageAccessUrl,
|
|
currentTriggerVersion,
|
|
currentTriggerVersionFetchFailed,
|
|
}) =>
|
|
({
|
|
enabled: true,
|
|
hasOrgIntegration,
|
|
authInvalid: false,
|
|
connectedProject,
|
|
isGitHubConnected,
|
|
hasStagingEnvironment,
|
|
hasPreviewEnvironment,
|
|
customEnvironments,
|
|
autoAssignCustomDomains,
|
|
vercelManageAccessUrl,
|
|
currentTriggerVersion,
|
|
currentTriggerVersionFetchFailed,
|
|
}) as VercelSettingsResult
|
|
);
|
|
}
|
|
)
|
|
.mapErr((error) => {
|
|
// Log the error and return a safe fallback
|
|
logger.error("Error in VercelSettingsPresenter.call", { error });
|
|
return error;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get data needed for the onboarding modal (custom environments and env vars)
|
|
*/
|
|
public async getOnboardingData(
|
|
projectId: string,
|
|
organizationId: string,
|
|
vercelEnvironmentId?: string
|
|
): Promise<VercelOnboardingData | null> {
|
|
const result = await ResultAsync.fromPromise(
|
|
(async (): Promise<VercelOnboardingData | null> => {
|
|
const [gitHubInstallations, connectedGitHubRepo] = await Promise.all([
|
|
(this._replica as PrismaClient).githubAppInstallation.findMany({
|
|
where: {
|
|
organizationId,
|
|
deletedAt: null,
|
|
suspendedAt: null,
|
|
},
|
|
select: {
|
|
id: true,
|
|
accountHandle: true,
|
|
targetType: true,
|
|
appInstallationId: true,
|
|
repositories: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
fullName: true,
|
|
htmlUrl: true,
|
|
private: true,
|
|
},
|
|
take: 200,
|
|
},
|
|
},
|
|
take: 20,
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
}),
|
|
(this._replica as PrismaClient).connectedGithubRepository.findFirst({
|
|
where: {
|
|
projectId,
|
|
repository: {
|
|
installation: {
|
|
deletedAt: null,
|
|
suspendedAt: null,
|
|
},
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const isGitHubConnected = connectedGitHubRepo !== null;
|
|
const gitHubAppInstallations: GitHubAppInstallation[] = gitHubInstallations.map(
|
|
(installation) => ({
|
|
id: installation.id,
|
|
appInstallationId: installation.appInstallationId,
|
|
targetType: installation.targetType,
|
|
accountHandle: installation.accountHandle,
|
|
repositories: installation.repositories.map((repo) => ({
|
|
id: repo.id,
|
|
name: repo.name,
|
|
fullName: repo.fullName,
|
|
private: repo.private,
|
|
htmlUrl: repo.htmlUrl,
|
|
})),
|
|
})
|
|
);
|
|
|
|
const orgIntegration = await (
|
|
this._replica as PrismaClient
|
|
).organizationIntegration.findFirst({
|
|
where: {
|
|
organizationId,
|
|
service: "VERCEL",
|
|
deletedAt: null,
|
|
},
|
|
include: {
|
|
tokenReference: true,
|
|
},
|
|
});
|
|
|
|
if (!orgIntegration) {
|
|
return null;
|
|
}
|
|
|
|
const tokenResult = await VercelIntegrationRepository.validateVercelToken(orgIntegration);
|
|
if (tokenResult.isErr() || !tokenResult.value.isValid) {
|
|
return {
|
|
customEnvironments: [],
|
|
environmentVariables: [],
|
|
availableProjects: [],
|
|
hasProjectSelected: false,
|
|
authInvalid: true,
|
|
authError: tokenResult.isErr() ? tokenResult.error.message : "Vercel token is invalid",
|
|
existingVariables: {},
|
|
gitHubAppInstallations,
|
|
isGitHubConnected,
|
|
isOnboardingComplete: false,
|
|
};
|
|
}
|
|
|
|
const clientResult =
|
|
await VercelIntegrationRepository.getVercelClientAndToken(orgIntegration);
|
|
if (clientResult.isErr()) {
|
|
return {
|
|
customEnvironments: [],
|
|
environmentVariables: [],
|
|
availableProjects: [],
|
|
hasProjectSelected: false,
|
|
authInvalid: clientResult.error.authInvalid,
|
|
authError: clientResult.error.authInvalid ? clientResult.error.message : undefined,
|
|
existingVariables: {},
|
|
gitHubAppInstallations,
|
|
isGitHubConnected,
|
|
isOnboardingComplete: false,
|
|
};
|
|
}
|
|
const { client, accessToken } = clientResult.value;
|
|
const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);
|
|
|
|
const projectIntegration = await (
|
|
this._replica as PrismaClient
|
|
).organizationProjectIntegration.findFirst({
|
|
where: {
|
|
projectId,
|
|
deletedAt: null,
|
|
organizationIntegration: {
|
|
service: "VERCEL",
|
|
deletedAt: null,
|
|
},
|
|
},
|
|
});
|
|
|
|
const availableProjectsResult = await VercelIntegrationRepository.getVercelProjects(
|
|
client,
|
|
teamId
|
|
);
|
|
|
|
if (availableProjectsResult.isErr()) {
|
|
return {
|
|
customEnvironments: [],
|
|
environmentVariables: [],
|
|
availableProjects: [],
|
|
hasProjectSelected: false,
|
|
authInvalid: availableProjectsResult.error.authInvalid,
|
|
authError: availableProjectsResult.error.authInvalid
|
|
? availableProjectsResult.error.message
|
|
: undefined,
|
|
existingVariables: {},
|
|
gitHubAppInstallations,
|
|
isGitHubConnected,
|
|
isOnboardingComplete: false,
|
|
};
|
|
}
|
|
|
|
if (!projectIntegration) {
|
|
return {
|
|
customEnvironments: [],
|
|
environmentVariables: [],
|
|
availableProjects: availableProjectsResult.value,
|
|
hasProjectSelected: false,
|
|
existingVariables: {},
|
|
gitHubAppInstallations,
|
|
isGitHubConnected,
|
|
isOnboardingComplete: false,
|
|
};
|
|
}
|
|
|
|
const [customEnvironmentsResult, projectEnvVarsResult, sharedEnvVarsResult] =
|
|
await Promise.all([
|
|
VercelIntegrationRepository.getVercelCustomEnvironments(
|
|
client,
|
|
projectIntegration.externalEntityId,
|
|
teamId
|
|
),
|
|
VercelIntegrationRepository.getVercelEnvironmentVariables(
|
|
client,
|
|
projectIntegration.externalEntityId,
|
|
teamId
|
|
),
|
|
// Only fetch shared env vars if teamId is available
|
|
teamId
|
|
? VercelIntegrationRepository.getVercelSharedEnvironmentVariables(
|
|
accessToken,
|
|
teamId,
|
|
projectIntegration.externalEntityId
|
|
)
|
|
: okAsync(
|
|
[] as Array<{
|
|
id: string;
|
|
key: string;
|
|
type: string;
|
|
isSecret: boolean;
|
|
target: string[];
|
|
}>
|
|
),
|
|
]);
|
|
const authInvalid =
|
|
(customEnvironmentsResult.isErr() && customEnvironmentsResult.error.authInvalid) ||
|
|
(projectEnvVarsResult.isErr() && projectEnvVarsResult.error.authInvalid) ||
|
|
(sharedEnvVarsResult.isErr() && sharedEnvVarsResult.error.authInvalid);
|
|
|
|
if (authInvalid) {
|
|
const authError =
|
|
(customEnvironmentsResult.isErr() &&
|
|
customEnvironmentsResult.error.authInvalid &&
|
|
customEnvironmentsResult.error.message) ||
|
|
(projectEnvVarsResult.isErr() &&
|
|
projectEnvVarsResult.error.authInvalid &&
|
|
projectEnvVarsResult.error.message) ||
|
|
(sharedEnvVarsResult.isErr() &&
|
|
sharedEnvVarsResult.error.authInvalid &&
|
|
sharedEnvVarsResult.error.message) ||
|
|
undefined;
|
|
|
|
return {
|
|
customEnvironments: [],
|
|
environmentVariables: [],
|
|
availableProjects: availableProjectsResult.value,
|
|
hasProjectSelected: true,
|
|
authInvalid: true,
|
|
authError: authError || undefined,
|
|
existingVariables: {},
|
|
gitHubAppInstallations,
|
|
isGitHubConnected,
|
|
isOnboardingComplete: false,
|
|
};
|
|
}
|
|
|
|
const customEnvironments = customEnvironmentsResult.isOk()
|
|
? customEnvironmentsResult.value
|
|
: [];
|
|
const projectEnvVars = projectEnvVarsResult.isOk() ? projectEnvVarsResult.value : [];
|
|
const sharedEnvVars = sharedEnvVarsResult.isOk() ? sharedEnvVarsResult.value : [];
|
|
|
|
// Hide platform-managed reserved keys from the onboarding preview.
|
|
const projectEnvVarKeys = new Set(projectEnvVars.map((v) => v.key));
|
|
const mergedEnvVars: VercelEnvironmentVariable[] = [
|
|
...projectEnvVars
|
|
.filter((v) => !isReservedForExternalSync(v.key))
|
|
.map((v) => {
|
|
const envVar = { ...v };
|
|
if (
|
|
vercelEnvironmentId &&
|
|
(v as any).customEnvironmentIds?.includes(vercelEnvironmentId)
|
|
) {
|
|
envVar.target = [...v.target, "staging"];
|
|
}
|
|
return envVar;
|
|
}),
|
|
...sharedEnvVars
|
|
.filter((v) => !projectEnvVarKeys.has(v.key) && !isReservedForExternalSync(v.key))
|
|
.map((v) => {
|
|
const envVar = {
|
|
id: v.id,
|
|
key: v.key,
|
|
type: v.type as VercelEnvironmentVariable["type"],
|
|
isSecret: v.isSecret,
|
|
target: v.target,
|
|
isShared: true,
|
|
customEnvironmentIds: [] as string[],
|
|
};
|
|
if (
|
|
vercelEnvironmentId &&
|
|
(v as any).customEnvironmentIds?.includes(vercelEnvironmentId)
|
|
) {
|
|
envVar.target = [...v.target, "staging"];
|
|
}
|
|
return envVar;
|
|
}),
|
|
];
|
|
|
|
const sortedEnvVars = [...mergedEnvVars].sort((a, b) => a.key.localeCompare(b.key));
|
|
|
|
const projectEnvs = await (this._replica as PrismaClient).runtimeEnvironment.findMany({
|
|
where: {
|
|
projectId,
|
|
archivedAt: null, // Filter out archived environments
|
|
},
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
type: true,
|
|
},
|
|
});
|
|
const envIdToSlug = new Map(projectEnvs.map((e) => [e.id, e.slug]));
|
|
const activeEnvIds = new Set(projectEnvs.map((e) => e.id));
|
|
|
|
const envVarRepository = new EnvironmentVariablesRepository(this._replica as PrismaClient);
|
|
const existingVariables = await envVarRepository.getProject(projectId);
|
|
const existingVariablesRecord: Record<string, { environments: string[] }> = {};
|
|
for (const v of existingVariables) {
|
|
// Filter out archived environments and map to slugs
|
|
const activeEnvSlugs = v.values
|
|
.filter((val) => activeEnvIds.has(val.environment.id))
|
|
.map(
|
|
(val) => envIdToSlug.get(val.environment.id) || val.environment.type.toLowerCase()
|
|
);
|
|
if (activeEnvSlugs.length > 0) {
|
|
existingVariablesRecord[v.key] = {
|
|
environments: activeEnvSlugs,
|
|
};
|
|
}
|
|
}
|
|
|
|
const parsedIntegrationData = VercelProjectIntegrationDataSchema.safeParse(
|
|
projectIntegration.integrationData
|
|
);
|
|
|
|
return {
|
|
customEnvironments,
|
|
environmentVariables: sortedEnvVars,
|
|
availableProjects: availableProjectsResult.value,
|
|
hasProjectSelected: true,
|
|
existingVariables: existingVariablesRecord,
|
|
gitHubAppInstallations,
|
|
isGitHubConnected,
|
|
isOnboardingComplete: parsedIntegrationData.success
|
|
? (parsedIntegrationData.data.onboardingCompleted ?? false)
|
|
: false,
|
|
};
|
|
})(),
|
|
(error) => error
|
|
);
|
|
|
|
if (result.isErr()) {
|
|
logger.error("Error in getOnboardingData", { error: result.error });
|
|
return null;
|
|
}
|
|
|
|
return result.value;
|
|
}
|
|
}
|