import type { PrismaClient, PrismaReplicaClient } from "~/db.server"; import { $replica, prisma } from "~/db.server"; import type { Project } from "~/models/project.server"; import type { User } from "~/models/user.server"; import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; import type { EnvironmentVariableUpdater } from "~/v3/environmentVariables/repository"; import type { SyncEnvVarsMapping, EnvSlug } from "~/v3/vercel/vercelProjectIntegrationSchema"; import { VercelIntegrationService } from "~/services/vercelIntegration.server"; import { loadEnvironmentVariablesEnvironments } from "./environmentVariablesEnvironments.server"; type Result = Awaited>; export type EnvironmentVariableWithSetValues = Result["environmentVariables"][number]; export class EnvironmentVariablesPresenter { #prismaClient: PrismaClient; #replicaClient: PrismaReplicaClient; constructor(prismaClient: PrismaClient = prisma, replicaClient: PrismaReplicaClient = $replica) { this.#prismaClient = prismaClient; this.#replicaClient = replicaClient; } public async call({ userId, projectSlug }: { userId: User["id"]; projectSlug: Project["slug"] }) { const project = await this.#replicaClient.project.findFirst({ select: { id: true, }, where: { slug: projectSlug, organization: { members: { some: { userId, }, }, }, }, }); if (!project) { throw new Error("Project not found"); } const { environments: sortedEnvironments, hasStaging } = await loadEnvironmentVariablesEnvironments( this.#replicaClient, { userId, projectId: project.id }, { skipProjectAccessCheck: true } ); // Only load values for the environments we display. Projects can accumulate // values in archived branch environments, which would otherwise all be loaded here. const environmentIds = sortedEnvironments.map((env) => env.id); const environmentVariables = await this.#replicaClient.environmentVariable.findMany({ select: { id: true, key: true, values: { select: { id: true, environmentId: true, version: true, lastUpdatedBy: true, updatedAt: true, valueReference: { select: { key: true, }, }, isSecret: true, }, where: { environmentId: { in: environmentIds, }, }, }, }, where: { projectId: project.id, }, }); const userIds = new Set( environmentVariables .flatMap((envVar) => envVar.values) .map((value) => value.lastUpdatedBy) .filter( (lastUpdatedBy): lastUpdatedBy is { type: "user"; userId: string } => lastUpdatedBy !== null && typeof lastUpdatedBy === "object" && "type" in lastUpdatedBy && lastUpdatedBy.type === "user" && "userId" in lastUpdatedBy && typeof lastUpdatedBy.userId === "string" ) .map((lastUpdatedBy) => lastUpdatedBy.userId) ); const users = userIds.size > 0 ? await this.#replicaClient.user.findMany({ where: { id: { in: Array.from(userIds), }, }, select: { id: true, name: true, displayName: true, avatarUrl: true, }, }) : []; const usersRecord: Record< string, { id: string; name: string | null; displayName: string | null; avatarUrl: string | null } > = Object.fromEntries(users.map((u) => [u.id, u])); const repository = new EnvironmentVariablesRepository(this.#prismaClient, this.#replicaClient); const nonSecretItems: Array<{ environmentId: string; key: string }> = []; for (const environmentVariable of environmentVariables) { for (const env of sortedEnvironments) { const valueRecord = environmentVariable.values.find((v) => v.environmentId === env.id); if (valueRecord && !valueRecord.isSecret) { nonSecretItems.push({ environmentId: env.id, key: environmentVariable.key }); } } } const variableValuesByEnvAndKey = await repository.getVariableValuesForKeys( project.id, nonSecretItems ); // Get Vercel integration data if it exists const vercelService = new VercelIntegrationService(this.#prismaClient); const vercelIntegration = await vercelService.getVercelProjectIntegration(project.id); let vercelSyncEnvVarsMapping: SyncEnvVarsMapping = {}; let vercelPullEnvVarsBeforeBuild: EnvSlug[] | null = null; if (vercelIntegration) { vercelSyncEnvVarsMapping = vercelIntegration.parsedIntegrationData.syncEnvVarsMapping; vercelPullEnvVarsBeforeBuild = vercelIntegration.parsedIntegrationData.config.pullEnvVarsBeforeBuild ?? null; } return { environmentVariables: environmentVariables .flatMap((environmentVariable) => { return sortedEnvironments.flatMap((env) => { const valueRecord = environmentVariable.values.find((v) => v.environmentId === env.id); const isSecret = valueRecord?.isSecret ?? false; if (!valueRecord) { return []; } const val = isSecret ? undefined : variableValuesByEnvAndKey.get(`${env.id}:${environmentVariable.key}`); if (!isSecret && val === undefined) { return []; } const lastUpdatedBy = valueRecord.lastUpdatedBy as EnvironmentVariableUpdater | null; const updatedByUser = lastUpdatedBy?.type === "user" ? (() => { const user = usersRecord[lastUpdatedBy.userId]; return user ? { id: user.id, name: user.displayName || user.name || "Unknown", avatarUrl: user.avatarUrl, } : null; })() : null; return [ { id: environmentVariable.id, key: environmentVariable.key, environment: { type: env.type, id: env.id, branchName: env.branchName }, value: isSecret ? "" : val!, isSecret, version: valueRecord.version, lastUpdatedBy, updatedByUser, updatedAt: valueRecord.updatedAt, }, ]; }); }) .sort((a, b) => a.key.localeCompare(b.key)), environments: sortedEnvironments, hasStaging, // Vercel integration data vercelIntegration: vercelIntegration ? { enabled: true, pullEnvVarsBeforeBuild: vercelPullEnvVarsBeforeBuild, syncEnvVarsMapping: vercelSyncEnvVarsMapping, } : null, }; } }