Skip to content

Commit 9a27e59

Browse files
committed
Introduce neverthrow for error handling
1 parent 0002bd6 commit 9a27e59

File tree

4 files changed

+152
-89
lines changed

4 files changed

+152
-89
lines changed

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

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page
4242
import { Paragraph } from "~/components/primitives/Paragraph";
4343
import * as Property from "~/components/primitives/PropertyTable";
4444
import { SpinnerWhite } from "~/components/primitives/Spinner";
45-
import { prisma } from "~/db.server";
4645
import { useOrganization } from "~/hooks/useOrganizations";
4746
import { useProject } from "~/hooks/useProject";
4847
import {
@@ -53,7 +52,6 @@ import {
5352
getSession,
5453
commitSession,
5554
} from "~/models/message.server";
56-
import { findProjectBySlug } from "~/models/project.server";
5755
import { ProjectSettingsService } from "~/services/projectSettings.server";
5856
import { logger } from "~/services/logger.server";
5957
import { requireUserId } from "~/services/session.server";
@@ -64,17 +62,16 @@ import {
6462
EnvironmentParamSchema,
6563
v3ProjectSettingsPath,
6664
} from "~/utils/pathBuilder";
67-
import React, { useEffect, useState } from "react";
65+
import { useEffect, useState } from "react";
6866
import { Select, SelectItem } from "~/components/primitives/Select";
6967
import { Switch } from "~/components/primitives/Switch";
70-
import { BranchTrackingConfigSchema, type BranchTrackingConfig } from "~/v3/github";
68+
import { type BranchTrackingConfig } from "~/v3/github";
7169
import {
7270
EnvironmentIcon,
7371
environmentFullTitle,
7472
environmentTextClassName,
7573
} from "~/components/environments/EnvironmentLabel";
7674
import { GitBranchIcon } from "lucide-react";
77-
import { env } from "~/env.server";
7875
import { useEnvironment } from "~/hooks/useEnvironment";
7976
import { DateTime } from "~/components/primitives/DateTime";
8077
import { TextLink } from "~/components/primitives/TextLink";
@@ -94,12 +91,37 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
9491
const { projectParam, organizationSlug } = EnvironmentParamSchema.parse(params);
9592

9693
const projectSettingsPresenter = new ProjectSettingsPresenter();
97-
const { gitHubApp } = await projectSettingsPresenter.getProjectSettings(
94+
const resultOrFail = await projectSettingsPresenter.getProjectSettings(
9895
organizationSlug,
9996
projectParam,
10097
userId
10198
);
10299

100+
if (resultOrFail.isErr()) {
101+
switch (resultOrFail.error.type) {
102+
case "project_not_found": {
103+
throw new Response(undefined, {
104+
status: 404,
105+
statusText: "Project not found",
106+
});
107+
}
108+
case "other":
109+
default: {
110+
resultOrFail.error.type satisfies "other";
111+
112+
logger.error("Failed loading project settings", {
113+
error: resultOrFail.error,
114+
});
115+
throw new Response(undefined, {
116+
status: 400,
117+
statusText: "Something went wrong, please try again!",
118+
});
119+
}
120+
}
121+
}
122+
123+
const { gitHubApp } = resultOrFail.value;
124+
103125
const session = await getSession(request.headers.get("Cookie"));
104126
const openGitHubRepoConnectionModal = session.get("gitHubAppInstalled") === true;
105127
const headers = new Headers({
@@ -278,7 +300,8 @@ export const action: ActionFunction = async ({ request, params }) => {
278300
}
279301
}
280302
default: {
281-
return submission.value satisfies never;
303+
submission.value satisfies never;
304+
return redirectBackWithErrorMessage(request, "Failed to process request");
282305
}
283306
}
284307
} catch (error: any) {
Lines changed: 111 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { type PrismaClient } from "@trigger.dev/database";
22
import { prisma } from "~/db.server";
3-
import { DeleteProjectService } from "~/services/deleteProject.server";
4-
import { BranchTrackingConfigSchema, type BranchTrackingConfig } from "~/v3/github";
5-
import { checkGitHubBranchExists } from "~/services/gitHub.server";
6-
import { tryCatch } from "@trigger.dev/core/utils";
3+
import { BranchTrackingConfigSchema } from "~/v3/github";
74
import { env } from "~/env.server";
85
import { findProjectBySlug } from "~/models/project.server";
6+
import { err, fromPromise, ok, okAsync } from "neverthrow";
97

108
export class ProjectSettingsPresenter {
119
#prismaClient: PrismaClient;
@@ -14,101 +12,133 @@ export class ProjectSettingsPresenter {
1412
this.#prismaClient = prismaClient;
1513
}
1614

17-
async getProjectSettings(organizationSlug: string, projectSlug: string, userId: string) {
15+
getProjectSettings(organizationSlug: string, projectSlug: string, userId: string) {
1816
const githubAppEnabled = env.GITHUB_APP_ENABLED === "1";
1917

2018
if (!githubAppEnabled) {
21-
return {
19+
return okAsync({
2220
gitHubApp: {
2321
enabled: false,
2422
connectedRepository: undefined,
2523
installations: undefined,
2624
},
27-
};
25+
});
2826
}
2927

30-
const project = await findProjectBySlug(organizationSlug, projectSlug, userId);
31-
if (!project) {
32-
throw new Error("Project not found");
33-
}
34-
const connectedGithubRepository = await prisma.connectedGithubRepository.findFirst({
35-
where: {
36-
projectId: project.id,
37-
},
38-
select: {
39-
branchTracking: true,
40-
previewDeploymentsEnabled: true,
41-
createdAt: true,
42-
repository: {
28+
const getProject = () =>
29+
fromPromise(findProjectBySlug(organizationSlug, projectSlug, userId), (error) => ({
30+
type: "other" as const,
31+
cause: error,
32+
})).andThen((project) => {
33+
if (!project) {
34+
return err({ type: "project_not_found" as const });
35+
}
36+
return ok(project);
37+
});
38+
39+
const findConnectedGithubRepository = (projectId: string) =>
40+
fromPromise(
41+
this.#prismaClient.connectedGithubRepository.findFirst({
42+
where: {
43+
projectId,
44+
},
4345
select: {
44-
id: true,
45-
name: true,
46-
fullName: true,
47-
htmlUrl: true,
48-
private: true,
46+
branchTracking: true,
47+
previewDeploymentsEnabled: true,
48+
createdAt: true,
49+
repository: {
50+
select: {
51+
id: true,
52+
name: true,
53+
fullName: true,
54+
htmlUrl: true,
55+
private: true,
56+
},
57+
},
4958
},
50-
},
51-
},
52-
});
59+
}),
60+
(error) => ({
61+
type: "other" as const,
62+
cause: error,
63+
})
64+
).map((connectedGithubRepository) => {
65+
if (!connectedGithubRepository) {
66+
return undefined;
67+
}
5368

54-
if (connectedGithubRepository) {
55-
const branchTrackingOrFailure = BranchTrackingConfigSchema.safeParse(
56-
connectedGithubRepository.branchTracking
57-
);
69+
const branchTrackingOrFailure = BranchTrackingConfigSchema.safeParse(
70+
connectedGithubRepository.branchTracking
71+
);
72+
return {
73+
...connectedGithubRepository,
74+
branchTracking: branchTrackingOrFailure.success
75+
? branchTrackingOrFailure.data
76+
: undefined,
77+
};
78+
});
5879

59-
return {
60-
gitHubApp: {
61-
enabled: true,
62-
connectedRepository: {
63-
...connectedGithubRepository,
64-
branchTracking: branchTrackingOrFailure.success
65-
? branchTrackingOrFailure.data
66-
: undefined,
80+
const listGithubAppInstallations = (organizationId: string) =>
81+
fromPromise(
82+
this.#prismaClient.githubAppInstallation.findMany({
83+
where: {
84+
organizationId,
85+
deletedAt: null,
86+
suspendedAt: null,
6787
},
68-
// skip loading installations if there is a connected repository
69-
// a project can have only a single connected repository
70-
installations: undefined,
71-
},
72-
};
73-
}
74-
75-
const githubAppInstallations = await prisma.githubAppInstallation.findMany({
76-
where: {
77-
organizationId: project.organizationId,
78-
deletedAt: null,
79-
suspendedAt: null,
80-
},
81-
select: {
82-
id: true,
83-
accountHandle: true,
84-
targetType: true,
85-
appInstallationId: true,
86-
repositories: {
8788
select: {
8889
id: true,
89-
name: true,
90-
fullName: true,
91-
htmlUrl: true,
92-
private: true,
90+
accountHandle: true,
91+
targetType: true,
92+
appInstallationId: true,
93+
repositories: {
94+
select: {
95+
id: true,
96+
name: true,
97+
fullName: true,
98+
htmlUrl: true,
99+
private: true,
100+
},
101+
// Most installations will only have a couple of repos so loading them here should be fine.
102+
// However, there might be outlier organizations so it's best to expose the installation repos
103+
// via a resource endpoint and filter on user input.
104+
take: 200,
105+
},
93106
},
94-
// Most installations will only have a couple of repos so loading them here should be fine.
95-
// However, there might be outlier organizations so it's best to expose the installation repos
96-
// via a resource endpoint and filter on user input.
97-
take: 200,
98-
},
99-
},
100-
take: 20,
101-
orderBy: {
102-
createdAt: "desc",
103-
},
104-
});
107+
take: 20,
108+
orderBy: {
109+
createdAt: "desc",
110+
},
111+
}),
112+
(error) => ({
113+
type: "other" as const,
114+
cause: error,
115+
})
116+
);
117+
118+
return getProject().andThen((project) =>
119+
findConnectedGithubRepository(project.id).andThen((connectedGithubRepository) => {
120+
if (connectedGithubRepository) {
121+
return okAsync({
122+
gitHubApp: {
123+
enabled: true,
124+
connectedRepository: connectedGithubRepository,
125+
// skip loading installations if there is a connected repository
126+
// a project can have only a single connected repository
127+
installations: undefined,
128+
},
129+
});
130+
}
105131

106-
return {
107-
gitHubApp: {
108-
enabled: true,
109-
connectedRepository: undefined,
110-
installations: githubAppInstallations,
111-
},
112-
};
132+
return listGithubAppInstallations(project.organizationId).map((githubAppInstallations) => {
133+
return {
134+
gitHubApp: {
135+
enabled: true,
136+
connectedRepository: undefined,
137+
installations: githubAppInstallations,
138+
},
139+
};
140+
});
141+
})
142+
);
113143
}
114144
}

apps/webapp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
"match-sorter": "^6.3.4",
160160
"morgan": "^1.10.0",
161161
"nanoid": "3.3.8",
162+
"neverthrow": "^8.2.0",
162163
"non.geist": "^1.0.2",
163164
"octokit": "^3.2.1",
164165
"ohash": "^1.1.3",

pnpm-lock.yaml

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)