217 lines
7.2 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|