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
6 changes: 6 additions & 0 deletions .server-changes/bound-checkschedule-environment-load.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Validating a schedule when deploying or updating a schedule now does less work on projects with many preview branches, so those operations stay fast as branches accumulate.
5 changes: 4 additions & 1 deletion apps/webapp/app/v3/services/checkSchedule.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getLimit } from "~/services/platform.v3.server";
import { getTimezones } from "~/utils/timezones.server";
import { env } from "~/env.server";
import type { ScheduleWindow } from "@trigger.dev/core/v3";
import { type PrismaClientOrTransaction } from "@trigger.dev/database";
import { boundedIn, type PrismaClientOrTransaction } from "@trigger.dev/database";
import { validateScheduleWindowSyntax } from "../scheduleWindow.server";

type Schedule = {
Expand Down Expand Up @@ -81,6 +81,9 @@ export class CheckScheduleService extends BaseService {
select: {
organizationId: true,
environments: {
where: {
id: { in: boundedIn(environmentIds) },
},
select: {
id: true,
type: true,
Expand Down
100 changes: 95 additions & 5 deletions apps/webapp/test/checkSchedule.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { containerTest } from "@internal/testcontainers";
import type { PrismaClient } from "@trigger.dev/database";
import { boundedIn, type PrismaClient } from "@trigger.dev/database";
import { describe, expect, vi } from "vitest";
import { resolveProjectScopedEnvironments } from "~/v3/services/resolveProjectScopedEnvironments";

vi.setConfig({ testTimeout: 60_000 });

// Exercises the environment-scoping primitive CheckScheduleService relies on
// (`resolveProjectScopedEnvironments`) with real RuntimeEnvironment rows,
// imported directly to avoid `~/db.server` and its eager global-prisma connect.

async function seedProjectWithEnv(prisma: PrismaClient, slugBase: string) {
const slug = `${slugBase}_${Math.random().toString(36).slice(2, 10)}`;
const organization = await prisma.organization.create({ data: { title: slug, slug } });
Expand All @@ -29,10 +25,47 @@ async function seedProjectWithEnv(prisma: PrismaClient, slugBase: string) {
return { organization, project, environment };
}

async function seedBranchEnv(
prisma: PrismaClient,
project: { id: string; organizationId: string },
slugBase: string,
{ archived }: { archived: boolean }
) {
const slug = `${slugBase}_${Math.random().toString(36).slice(2, 10)}`;
return prisma.runtimeEnvironment.create({
data: {
slug: `${slug}-branch`,
type: "PREVIEW",
branchName: slug,
projectId: project.id,
organizationId: project.organizationId,
apiKey: `tr_preview_${slug}`,
pkApiKey: `pk_preview_${slug}`,
shortcode: Math.random().toString(36).slice(2, 10),
archivedAt: archived ? new Date() : null,
},
});
}

function projectEnvironments(prisma: PrismaClient, projectId: string) {
return prisma.runtimeEnvironment.findMany({ where: { projectId }, select: { id: true } });
}

function loadScopedEnvironments(prisma: PrismaClient, projectId: string, environmentIds: string[]) {
return prisma.project
.findFirst({
where: { id: projectId },
select: {
organizationId: true,
environments: {
where: { id: { in: boundedIn(environmentIds) } },
select: { id: true, type: true, archivedAt: true },
},
},
})
.then((project) => project?.environments ?? []);
}

describe("resolveProjectScopedEnvironments (schedule env scoping)", () => {
containerTest("rejects an environment id that belongs to another project", async ({ prisma }) => {
const a = await seedProjectWithEnv(prisma, "orga");
Expand All @@ -58,3 +91,60 @@ describe("resolveProjectScopedEnvironments (schedule env scoping)", () => {
expect(result.kind).toBe("ok");
});
});

describe("CheckScheduleService bounded environments load", () => {
containerTest(
"loads only the requested environments, not every project environment",
async ({ prisma }) => {
const a = await seedProjectWithEnv(prisma, "orga");
for (let i = 0; i < 8; i++) {
await seedBranchEnv(prisma, a.project, `branch${i}`, { archived: true });
}
await seedBranchEnv(prisma, a.project, "active", { archived: false });

const all = await projectEnvironments(prisma, a.project.id);
expect(all.length).toBe(10);

const scoped = await loadScopedEnvironments(prisma, a.project.id, [a.environment.id]);
expect(scoped.length).toBe(1);
expect(scoped[0]?.id).toBe(a.environment.id);

const result = resolveProjectScopedEnvironments([a.environment.id], scoped);
expect(result.kind).toBe("ok");
}
);

containerTest(
"still rejects a foreign environment id when the load is bounded",
async ({ prisma }) => {
const a = await seedProjectWithEnv(prisma, "orga");
const b = await seedProjectWithEnv(prisma, "orgb");
await seedBranchEnv(prisma, a.project, "branch", { archived: true });

const scoped = await loadScopedEnvironments(prisma, a.project.id, [
a.environment.id,
b.environment.id,
]);
expect(scoped.length).toBe(1);

const result = resolveProjectScopedEnvironments([a.environment.id, b.environment.id], scoped);
expect(result.kind).toBe("foreign");
expect(result).toMatchObject({ foreignEnvironmentId: b.environment.id });
}
);

containerTest(
"still surfaces an archived branch env when it is the requested one",
async ({ prisma }) => {
const a = await seedProjectWithEnv(prisma, "orga");
const archivedBranch = await seedBranchEnv(prisma, a.project, "branch", { archived: true });

const scoped = await loadScopedEnvironments(prisma, a.project.id, [archivedBranch.id]);
expect(scoped.length).toBe(1);

const result = resolveProjectScopedEnvironments([archivedBranch.id], scoped);
expect(result.kind).toBe("ok");
expect(result.kind === "ok" && result.environments.some((env) => env.archivedAt)).toBe(true);
}
);
});
Loading