Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---

Speed up the environment variables page for projects with many archived preview branches. The page now only loads variable values for the environments it displays instead of every value ever created, including those left behind by archived branches.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaClient, prisma } from "~/db.server";
import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server";
import { Project } from "~/models/project.server";
import { User } from "~/models/user.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
Expand All @@ -15,13 +15,15 @@ export type EnvironmentVariableWithSetValues = Result["environmentVariables"][nu

export class EnvironmentVariablesPresenter {
#prismaClient: PrismaClient;
#replicaClient: PrismaReplicaClient;

constructor(prismaClient: PrismaClient = prisma) {
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.#prismaClient.project.findFirst({
const project = await this.#replicaClient.project.findFirst({
select: {
id: true,
},
Expand All @@ -41,7 +43,18 @@ export class EnvironmentVariablesPresenter {
throw new Error("Project not found");
}

const environmentVariables = await this.#prismaClient.environmentVariable.findMany({
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,
Expand All @@ -59,6 +72,11 @@ export class EnvironmentVariablesPresenter {
},
isSecret: true,
},
where: {
environmentId: {
in: environmentIds,
},
},
},
},
where: {
Expand All @@ -84,7 +102,7 @@ export class EnvironmentVariablesPresenter {

const users =
userIds.size > 0
? await this.#prismaClient.user.findMany({
? await this.#replicaClient.user.findMany({
where: {
id: {
in: Array.from(userIds),
Expand All @@ -102,14 +120,7 @@ export class EnvironmentVariablesPresenter {
const usersRecord: Record<string, { id: string; name: string | null; displayName: string | null; avatarUrl: string | null }> =
Object.fromEntries(users.map((u) => [u.id, u]));

const { environments: sortedEnvironments, hasStaging } =
await loadEnvironmentVariablesEnvironments(
this.#prismaClient,
{ userId, projectId: project.id },
{ skipProjectAccessCheck: true }
);

const repository = new EnvironmentVariablesRepository(this.#prismaClient);
const repository = new EnvironmentVariablesRepository(this.#prismaClient, this.#replicaClient);

const nonSecretItems: Array<{ environmentId: string; key: string }> = [];
for (const environmentVariable of environmentVariables) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
import { type PrismaClient } from "~/db.server";
import { type PrismaReplicaClient } from "~/db.server";
import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort";

export type EnvironmentVariablesEnvironment = {
Expand All @@ -15,7 +15,7 @@ export type EnvironmentVariablesEnvironmentsResult = {
};

export async function loadEnvironmentVariablesEnvironments(
prismaClient: PrismaClient,
prismaClient: PrismaReplicaClient,
{ userId, projectId }: { userId: string; projectId: string },
options?: { skipProjectAccessCheck?: boolean }
): Promise<EnvironmentVariablesEnvironmentsResult> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ export class EnvironmentVariablesRepository implements Repository {
}

const secretStore = getSecretStore("DATABASE", {
prismaClient: this.prismaClient,
prismaClient: this.replicaClient,
});

const storeKeys = Array.from(uniqueItems.values()).map((item) =>
Expand Down
110 changes: 109 additions & 1 deletion apps/webapp/test/EnvironmentVariablesPresenter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe("EnvironmentVariablesPresenter", () => {
userId: user.id,
});

const result = await new EnvironmentVariablesPresenter(prisma).call({
const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({
userId: user.id,
projectSlug,
});
Expand All @@ -69,4 +69,112 @@ describe("EnvironmentVariablesPresenter", () => {
expect(secretVariable!.value).toBe("");
expect(nonSecretVariable!.value).toBe("plain-value");
});

postgresTest(
"returns values for active environments (including branch environments) and excludes archived branch environments",
async ({ prisma }) => {
const { user, organization, project, projectSlug } =
await createTestOrgProjectWithMember(prisma);

const prodEnvironment = await createRuntimeEnvironment(prisma, {
projectId: project.id,
organizationId: organization.id,
type: "PRODUCTION",
});

const parentPreviewEnvironment = await createRuntimeEnvironment(prisma, {
projectId: project.id,
organizationId: organization.id,
type: "PREVIEW",
});
await prisma.runtimeEnvironment.update({
where: { id: parentPreviewEnvironment.id },
data: { isBranchableEnvironment: true },
});

const activeBranchEnvironment = await createRuntimeEnvironment(prisma, {
projectId: project.id,
organizationId: organization.id,
type: "PREVIEW",
});
await prisma.runtimeEnvironment.update({
where: { id: activeBranchEnvironment.id },
data: {
parentEnvironmentId: parentPreviewEnvironment.id,
branchName: "feature/active",
},
});

const archivedBranchEnvironment = await createRuntimeEnvironment(prisma, {
projectId: project.id,
organizationId: organization.id,
type: "PREVIEW",
});
await prisma.runtimeEnvironment.update({
where: { id: archivedBranchEnvironment.id },
data: {
parentEnvironmentId: parentPreviewEnvironment.id,
branchName: "feature/archived",
},
});

const repository = new EnvironmentVariablesRepository(prisma, prisma);

await createEnvironmentVariable(repository, project.id, {
environmentId: prodEnvironment.id,
key: "MY_VAR",
value: "prod-value",
userId: user.id,
});
await createEnvironmentVariable(repository, project.id, {
environmentId: activeBranchEnvironment.id,
key: "MY_VAR",
value: "active-branch-value",
userId: user.id,
});
await createEnvironmentVariable(repository, project.id, {
environmentId: archivedBranchEnvironment.id,
key: "MY_VAR",
value: "archived-branch-value",
userId: user.id,
});

// Archive the branch after it accumulated values (archiving does not
// delete its EnvironmentVariableValue rows).
await prisma.runtimeEnvironment.update({
where: { id: archivedBranchEnvironment.id },
data: { archivedAt: new Date() },
});

const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({
userId: user.id,
projectSlug,
});

const environmentIds = result.environments.map((environment) => environment.id);
expect(environmentIds).toContain(prodEnvironment.id);
expect(environmentIds).toContain(activeBranchEnvironment.id);
expect(environmentIds).not.toContain(archivedBranchEnvironment.id);

const myVarValues = result.environmentVariables.filter(
(variable) => variable.key === "MY_VAR"
);
expect(myVarValues).toHaveLength(2);

const prodValue = myVarValues.find(
(variable) => variable.environment.id === prodEnvironment.id
);
expect(prodValue?.value).toBe("prod-value");

const activeBranchValue = myVarValues.find(
(variable) => variable.environment.id === activeBranchEnvironment.id
);
expect(activeBranchValue?.value).toBe("active-branch-value");
expect(activeBranchValue?.environment.branchName).toBe("feature/active");

expect(
myVarValues.some((variable) => variable.environment.id === archivedBranchEnvironment.id)
).toBe(false);
}
);
});
Loading