chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,743 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user