Files
triggerdotdev--trigger.dev/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts
2026-07-13 13:32:57 +08:00

217 lines
7.2 KiB
TypeScript

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<ReturnType<EnvironmentVariablesPresenter["call"]>>;
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,
};
}
}