Skip to content

Commit 6731464

Browse files
committed
some fixes and tests
1 parent dacd9d6 commit 6731464

11 files changed

Lines changed: 369 additions & 96 deletions

File tree

apps/webapp/app/presenters/OrganizationsPresenter.server.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@ import { prisma } from "~/db.server";
44
import { logger } from "~/services/logger.server";
55
import { type UserFromSession } from "~/services/session.server";
66
import { newOrganizationPath, newProjectPath } from "~/utils/pathBuilder";
7-
import {
8-
SelectBestEnvironmentPresenter,
9-
type MinimumEnvironment,
10-
} from "./SelectBestEnvironmentPresenter.server";
7+
import { SelectBestEnvironmentPresenter } from "./SelectBestEnvironmentPresenter.server";
118
import { sortEnvironments } from "~/utils/environmentSort";
129
import { defaultAvatar, parseAvatar } from "~/components/primitives/Avatar";
1310
import { env } from "~/env.server";
1411
import { flags } from "~/v3/featureFlags.server";
1512
import { validatePartialFeatureFlags } from "~/v3/featureFlags";
16-
import { devPresence } from "./v3/DevPresence.server";
1713
import { hydrateEnvsWithActivity } from "./v3/BranchesPresenter.server";
1814

1915
export class OrganizationsPresenter {
@@ -85,6 +81,7 @@ export class OrganizationsPresenter {
8581
branchName: true,
8682
parentEnvironmentId: true,
8783
archivedAt: true,
84+
updatedAt: true,
8885
orgMember: {
8986
select: {
9087
userId: true,
@@ -104,8 +101,6 @@ export class OrganizationsPresenter {
104101
throw redirect(newProjectPath(organization));
105102
}
106103

107-
const recentDevBranchIds = await devPresence.getRecentBranchIds(user.id, fullProject.id);
108-
109104
const environments = fullProject.
110105
environments.filter((env) => env.type !== "DEVELOPMENT" || env.orgMember?.userId === user.id);
111106

apps/webapp/app/presenters/v3/BranchesPresenter.server.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GitMeta, } from "@trigger.dev/core/v3";
1+
import { GitMeta } from "@trigger.dev/core/v3";
22
import { DEFAULT_DEV_BRANCH } from "@trigger.dev/core/v3/utils/gitBranch";
33
import { type RuntimeEnvironmentType } from "@trigger.dev/database";
44
import { type z } from "zod";
@@ -10,13 +10,46 @@ import { getCurrentPlan, getPlans } from "~/services/platform.v3.server";
1010
import { checkBranchLimit } from "~/services/upsertBranch.server";
1111
import { devPresence } from "./DevPresence.server";
1212
import { sortEnvironments } from "~/utils/environmentSort";
13-
import { toBranchableEnvironmentType } from "~/utils/branchableEnvironment";
13+
import {
14+
type BranchableEnvironmentType,
15+
toBranchableEnvironmentType,
16+
} from "~/utils/branchableEnvironment";
1417

1518
type Result = Awaited<ReturnType<BranchesPresenter["call"]>>;
1619
export type Branch = Result["branches"][number];
1720

1821
const BRANCHES_PER_PAGE = 25;
1922

23+
/**
24+
* Prisma `where` fragment that scopes the branches list by branch name, keyed by
25+
* environment type. Spread it into the query's `where` (it contributes either a
26+
* `branchName` constraint or a top-level `OR`).
27+
*
28+
* The default DEV branch is the root dev env, stored with `branchName: null`, so
29+
* for DEVELOPMENT we always include the null-branchName root (and still match it
30+
* when searching — hence the top-level `OR`, since a scalar field filter can't
31+
* express "matches search OR is null"). PREVIEW only ever lists real branches, so
32+
* its root (null) is excluded. Passing no `search` yields the "all branches of
33+
* this type" fragment.
34+
*/
35+
function branchNameFilter(
36+
envType: BranchableEnvironmentType,
37+
search?: string
38+
): Prisma.RuntimeEnvironmentWhereInput {
39+
switch (envType) {
40+
case "DEVELOPMENT":
41+
return search
42+
? { OR: [{ branchName: { contains: search, mode: "insensitive" } }, { branchName: null }] }
43+
: {};
44+
case "PREVIEW":
45+
return search
46+
? { branchName: { contains: search, mode: "insensitive" } }
47+
: { branchName: { not: null } };
48+
default:
49+
throw new Error(`branchNameFilter: unsupported environment type "${envType}"`);
50+
}
51+
}
52+
2053
type Options = z.infer<typeof BranchesOptions>;
2154

2255
export type GitMetaLinks = {
@@ -133,30 +166,26 @@ export class BranchesPresenter {
133166
};
134167
}
135168

136-
// The default DEV branch has no branchName (it's the root dev env, stored
137-
// with branchName: null), so searching for it by name wouldn't display it.
138-
// Hacky way around that: always include the null-branchName root env.
139-
const branchNameWhere = envType === "DEVELOPMENT" ?
140-
search
141-
? { OR: [{ contains: search, mode: "insensitive" as const }, { is: null }] }
142-
: {} :
143-
search
144-
? { contains: search, mode: "insensitive" as const }
145-
: { not: null };
169+
const branchNameWhere = branchNameFilter(envType, search);
146170
const orgMemberWhere = envType === "DEVELOPMENT" ? { orgMember: { userId } } : {};
147171

148-
149172
const visibleCount = await this.#prismaClient.runtimeEnvironment.count({
150173
where: {
151174
projectId: project.id,
152175
type: envType,
153-
branchName: branchNameWhere,
176+
...branchNameWhere,
154177
...orgMemberWhere,
155178
...(showArchived ? {} : { archivedAt: null }),
156179
},
157180
});
158181

159-
const limits = await checkBranchLimit({ prisma: this.#prismaClient, organizationId: project.organizationId, projectId: project.id, userId, type: envType });
182+
const limits = await checkBranchLimit({
183+
prisma: this.#prismaClient,
184+
organizationId: project.organizationId,
185+
projectId: project.id,
186+
userId,
187+
type: envType,
188+
});
160189

161190
const [currentPlan, plans] = await Promise.all([
162191
getCurrentPlan(project.organizationId),
@@ -179,12 +208,13 @@ export class BranchesPresenter {
179208
type: true,
180209
archivedAt: true,
181210
createdAt: true,
211+
updatedAt: true,
182212
git: true,
183213
},
184214
where: {
185215
projectId: project.id,
186216
type: envType,
187-
branchName: branchNameWhere,
217+
...branchNameWhere,
188218
...orgMemberWhere,
189219
...(showArchived ? {} : { archivedAt: null }),
190220
},
@@ -195,17 +225,15 @@ export class BranchesPresenter {
195225
take: BRANCHES_PER_PAGE,
196226
});
197227

198-
const totalBranchesWhere = envType === "DEVELOPMENT" ? {} : { not: null };
199228
const totalBranches = await this.#prismaClient.runtimeEnvironment.count({
200229
where: {
201230
projectId: project.id,
202231
type: envType,
203-
branchName: totalBranchesWhere,
232+
...branchNameFilter(envType),
204233
...orgMemberWhere,
205234
},
206235
});
207236

208-
209237
const branchesFiltered = branches
210238
.filter((branch) => envType === "DEVELOPMENT" || branch.branchName !== null)
211239
.map((branch) => ({
@@ -234,8 +262,13 @@ export class BranchesPresenter {
234262
}
235263
}
236264

237-
export async function hydrateEnvsWithActivity<T extends { type: RuntimeEnvironmentType; id: string }>
238-
(userId: string, projectId: string, environments: T[]): Promise<Array<T & { lastActivity: Date | undefined; isConnected: boolean | undefined }>> {
265+
export async function hydrateEnvsWithActivity<
266+
T extends { type: RuntimeEnvironmentType; id: string }
267+
>(
268+
userId: string,
269+
projectId: string,
270+
environments: T[]
271+
): Promise<Array<T & { lastActivity: Date | undefined; isConnected: boolean | undefined }>> {
239272
const recentDevBranchIds = await devPresence.getRecentBranchIds(userId, projectId);
240273

241274
// Resolve presence for all recently-active dev branches in a single MGET

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev-branches/route.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,7 @@ import { logger } from "~/services/logger.server";
6363
import { requireUserId } from "~/services/session.server";
6464
import { UpsertBranchService } from "~/services/upsertBranch.server";
6565
import { cn } from "~/utils/cn";
66-
import {
67-
branchesDevPath,
68-
branchesPath,
69-
docsPath,
70-
ProjectParamSchema,
71-
} from "~/utils/pathBuilder";
66+
import { branchesDevPath, docsPath, ProjectParamSchema } from "~/utils/pathBuilder";
7267
import { ArchiveButton } from "../resources.branches.archive";
7368
import { IconArrowBearRight2 } from "@tabler/icons-react";
7469

@@ -110,7 +105,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
110105

111106
return typedjson(result);
112107
} catch (error) {
113-
logger.error("Error loading preview branches page", { error });
108+
logger.error("Error loading dev branches page", { error });
114109
throw new Response(undefined, {
115110
status: 400,
116111
statusText: "Something went wrong, if this problem persists please contact support.",
@@ -146,7 +141,7 @@ export async function action({ request }: ActionFunctionArgs) {
146141
}
147142

148143
return redirectWithSuccessMessage(
149-
`${branchesPath(result.organization, result.project, result.branch)}?dialogClosed=true`,
144+
`${branchesDevPath(result.organization, result.project, result.branch)}?dialogClosed=true`,
150145
request,
151146
`Branch "${result.branch.branchName}" created`
152147
);

apps/webapp/app/services/apiAuth.server.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -320,26 +320,26 @@ function getApiKeyResult(apiKey: string): {
320320
const type = isPublicApiKey(apiKey)
321321
? "PUBLIC"
322322
: isSecretApiKey(apiKey)
323-
? "PRIVATE"
324-
: isPublicJWT(apiKey)
325-
? "PUBLIC_JWT"
326-
: "PRIVATE"; // Fallback to private key
323+
? "PRIVATE"
324+
: isPublicJWT(apiKey)
325+
? "PUBLIC_JWT"
326+
: "PRIVATE"; // Fallback to private key
327327
return { apiKey, type };
328328
}
329329

330330
export type AuthenticationResult =
331331
| {
332-
type: "personalAccessToken";
333-
result: PersonalAccessTokenAuthenticationResult;
334-
}
332+
type: "personalAccessToken";
333+
result: PersonalAccessTokenAuthenticationResult;
334+
}
335335
| {
336-
type: "organizationAccessToken";
337-
result: OrganizationAccessTokenAuthenticationResult;
338-
}
336+
type: "organizationAccessToken";
337+
result: OrganizationAccessTokenAuthenticationResult;
338+
}
339339
| {
340-
type: "apiKey";
341-
result: ApiAuthenticationResult;
342-
};
340+
type: "apiKey";
341+
result: ApiAuthenticationResult;
342+
};
343343

344344
type AuthenticationMethod = "personalAccessToken" | "organizationAccessToken" | "apiKey";
345345

@@ -356,11 +356,11 @@ type FilteredAuthenticationResult<
356356
T extends AllowedAuthenticationMethods = AllowedAuthenticationMethods
357357
> =
358358
| (T["personalAccessToken"] extends true
359-
? Extract<AuthenticationResult, { type: "personalAccessToken" }>
360-
: never)
359+
? Extract<AuthenticationResult, { type: "personalAccessToken" }>
360+
: never)
361361
| (T["organizationAccessToken"] extends true
362-
? Extract<AuthenticationResult, { type: "organizationAccessToken" }>
363-
: never)
362+
? Extract<AuthenticationResult, { type: "organizationAccessToken" }>
363+
: never)
364364
| (T["apiKey"] extends true ? Extract<AuthenticationResult, { type: "apiKey" }> : never);
365365

366366
/**
@@ -523,10 +523,10 @@ export async function authenticatedEnvironmentForAuthentication(
523523
slug: slug,
524524
...(slug === "dev"
525525
? {
526-
orgMember: {
527-
userId: user.id,
528-
},
529-
}
526+
orgMember: {
527+
userId: user.id,
528+
},
529+
}
530530
: {}),
531531
},
532532
include: authIncludeBase,

apps/webapp/app/services/upsertBranch.server.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { type z } from "zod";
1515
import invariant from "tiny-invariant";
1616
import { type CreateBranchOptions } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route";
1717

18-
1918
type CreateBranchOptions = z.infer<typeof CreateBranchOptions>;
2019

2120
export class UpsertBranchService {
@@ -35,8 +34,6 @@ export class UpsertBranchService {
3534
| { type: "orgId"; organizationId: string },
3635
{ projectId, env, branchName, git }: CreateBranchOptions
3736
) {
38-
39-
4037
const parentEnvType = toBranchableEnvironmentType(env);
4138
// Dev branch creation is always user-scoped (org tokens are rejected upstream),
4239
// so we can disambiguate the per-member dev root by userId.
@@ -67,12 +64,12 @@ export class UpsertBranchService {
6764
organization:
6865
orgFilter.type === "userMembership"
6966
? {
70-
members: {
71-
some: {
72-
userId: orgFilter.userId,
67+
members: {
68+
some: {
69+
userId: orgFilter.userId,
70+
},
7371
},
74-
},
75-
}
72+
}
7673
: { id: orgFilter.organizationId },
7774
},
7875
include: {
@@ -94,7 +91,6 @@ export class UpsertBranchService {
9491

9592
// Dev environments are scoped per org member, so a dev branch must inherit
9693
// its parent's orgMemberId. Preview parents have no orgMember (orgMemberId is null).
97-
9894
if (!parentEnvironment) {
9995
invariant(env === "preview", "No default dev runtime environment setup");
10096
return {
@@ -110,15 +106,25 @@ export class UpsertBranchService {
110106
};
111107
}
112108

113-
114-
115-
const limits = await checkBranchLimit(
116-
{ prisma: this.#prismaClient, organizationId: parentEnvironment.organization.id, projectId: parentEnvironment.project.id, type: parentEnvType, userId, newBranchName: sanitizedBranchName });
109+
const limits = await checkBranchLimit({
110+
prisma: this.#prismaClient,
111+
organizationId: parentEnvironment.organization.id,
112+
projectId: parentEnvironment.project.id,
113+
type: parentEnvType,
114+
userId,
115+
newBranchName: sanitizedBranchName,
116+
});
117117

118118
if (limits.isAtLimit) {
119+
// DEVELOPMENT has no upgrade path, so only PREVIEW mentions upgrading.
120+
const remediation =
121+
parentEnvType === "PREVIEW"
122+
? "Use the CLI to view your existing branches and archive any you no longer need, or upgrade to get more."
123+
: "Use the CLI to view your existing branches and archive any you no longer need.";
124+
119125
return {
120126
success: false as const,
121-
error: `You've used all ${limits.used} of ${limits.limit} branches for your plan. Upgrade to get more branches or archive some.`,
127+
error: `You've used all ${limits.used} of ${limits.limit} branches for your plan. ${remediation}`,
122128
};
123129
}
124130

@@ -128,7 +134,6 @@ export class UpsertBranchService {
128134
const shortcode = branchSlug;
129135

130136
const now = new Date();
131-
132137
const branch = await this.#prismaClient.runtimeEnvironment.upsert({
133138
where: {
134139
projectId_shortcode: {
@@ -184,10 +189,21 @@ export class UpsertBranchService {
184189
}
185190
}
186191

187-
export async function checkBranchLimit(
188-
{ prisma, organizationId, projectId, userId, type, newBranchName }:
189-
{ prisma: PrismaClientOrTransaction; organizationId: string; projectId: string; userId?: string; type: BranchableEnvironmentType; newBranchName?: string; }) {
190-
192+
export async function checkBranchLimit({
193+
prisma,
194+
organizationId,
195+
projectId,
196+
userId,
197+
type,
198+
newBranchName,
199+
}: {
200+
prisma: PrismaClientOrTransaction;
201+
organizationId: string;
202+
projectId: string;
203+
userId?: string;
204+
type: BranchableEnvironmentType;
205+
newBranchName?: string;
206+
}) {
191207
let orgMemberWhere = {};
192208
if (type === "DEVELOPMENT") {
193209
invariant(userId, "Cannot use org access for dev server");

0 commit comments

Comments
 (0)