Skip to content

Commit 3bf2a8f

Browse files
refactor: replace TRPCError with ErrorWithCode in packages/features (calcom#25482)
* refactor: replace TRPCError with ErrorWithCode in packages/features This refactor moves error handling from throwing TRPCError directly in packages/features to throwing ErrorWithCode instead. The conversion to TRPCError now happens at the TRPC layer. Changes: - Add generic ErrorCode values (Unauthorized, Forbidden, NotFound, BadRequest, InternalServerError) to errorCodes.ts - Update getServerErrorFromUnknown to map new ErrorCodes to proper HTTP status codes - Create toTRPCError helper in packages/trpc/server/lib - Create errorMappingMiddleware in packages/trpc/server/middlewares - Migrate TRPCError throws in packages/features to ErrorWithCode: - teamService.ts - getEventTypeById.ts - eventTypeRepository.ts - OrganizationPermissionService.ts - OrganizationPaymentService.ts - sso.ts - handleCreatePhoneCall.ts - userCanCreateTeamGroupMapping.ts This improves separation of concerns by making packages/features transport-agnostic, allowing the same feature code to be reused from tRPC, API routes, workers, etc. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: remove isTrpcCall parameter and fix lint warning - Remove isTrpcCall parameter from get.handler.ts call since the feature layer no longer needs to know about tRPC - Fix unsafe optional chaining lint warning in getEventTypesByViewer.ts by precomputing usersSource variable - Complete migration of getEventTypesByViewer.ts to ErrorWithCode Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * revert * add eslint rule * add comment * fix: add isTrpcCall back to getEventTypeById interface The user reverted the removal of isTrpcCall parameter from the handler, so we need to add it back to the interface to fix the type error. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * test: update teamService tests to expect ErrorWithCode instead of TRPCError Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor * wip * feat: integrate errorMappingMiddleware into base TRPC procedure Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * connect middlewares * revert * revert * refactor * rename * fix: handle ErrorWithCode in teams server-page error handling The error handling was checking for TRPCError, but teamService now throws ErrorWithCode. This caused the 'This invitation is not for your account' error message to not be displayed when a wrong user tries to use an invitation link. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix * fix * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 16a4c75 commit 3bf2a8f

18 files changed

Lines changed: 280 additions & 120 deletions

File tree

apps/web/app/(use-page-wrapper)/(main-nav)/teams/server-page.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing"
66
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
77
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
88
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
9+
import { ErrorWithCode } from "@calcom/lib/errors";
910
import prisma from "@calcom/prisma";
1011
import { MembershipRole } from "@calcom/prisma/enums";
1112

12-
import { TRPCError } from "@trpc/server";
13-
1413
import { TeamsCTA } from "./CTA";
1514

1615
const getCachedTeams = unstable_cache(
@@ -52,7 +51,7 @@ export const ServerTeamsListing = async ({
5251
}
5352
} catch (e) {
5453
errorMsgFromInvite = "Error while fetching teams";
55-
if (e instanceof TRPCError) errorMsgFromInvite = e.message;
54+
if (e instanceof ErrorWithCode) errorMsgFromInvite = e.message;
5655
}
5756
}
5857

packages/features/ee/dsync/lib/server/userCanCreateTeamGroupMapping.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
import { canAccessOrganization } from "@calcom/features/ee/sso/lib/saml";
22
import prisma from "@calcom/prisma";
33
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
4-
5-
import { TRPCError } from "@trpc/server";
4+
import { ErrorCode } from "@calcom/lib/errorCodes";
5+
import { ErrorWithCode } from "@calcom/lib/errors";
66

77
const userCanCreateTeamGroupMapping = async (
88
user: NonNullable<TrpcSessionUser>,
99
organizationId: number | null,
1010
teamId?: number
1111
) => {
1212
if (!organizationId) {
13-
throw new TRPCError({
14-
code: "BAD_REQUEST",
15-
message: "Could not find organization id",
16-
});
13+
throw new ErrorWithCode(ErrorCode.BadRequest, "Could not find organization id");
1714
}
1815

1916
const { message, access } = await canAccessOrganization(user, organizationId);
2017
if (!access) {
21-
throw new TRPCError({
22-
code: "BAD_REQUEST",
23-
message,
24-
});
18+
throw new ErrorWithCode(ErrorCode.BadRequest, message);
2519
}
2620

2721
if (teamId) {
@@ -36,10 +30,7 @@ const userCanCreateTeamGroupMapping = async (
3630
});
3731

3832
if (!orgTeam) {
39-
throw new TRPCError({
40-
code: "BAD_REQUEST",
41-
message: "Could not find team",
42-
});
33+
throw new ErrorWithCode(ErrorCode.BadRequest, "Could not find team");
4334
}
4435
}
4536

packages/features/ee/organizations/lib/OrganizationPaymentService.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { prisma } from "@calcom/prisma";
1313
import type { OrganizationOnboarding } from "@calcom/prisma/client";
1414
import { UserPermissionRole, type BillingPeriod } from "@calcom/prisma/enums";
1515
import { userMetadata } from "@calcom/prisma/zod-utils";
16-
17-
import { TRPCError } from "@trpc/server";
16+
import { ErrorCode } from "@calcom/lib/errorCodes";
17+
import { ErrorWithCode } from "@calcom/lib/errors";
1818

1919
import { OrganizationPermissionService } from "./OrganizationPermissionService";
2020
import type { OnboardingUser } from "./service/onboarding/types";
@@ -180,10 +180,10 @@ export class OrganizationPaymentService {
180180
this.permissionService.hasModifiedDefaultPayment(input) &&
181181
!this.permissionService.hasPermissionToModifyDefaultPayment()
182182
) {
183-
throw new TRPCError({
184-
code: "UNAUTHORIZED",
185-
message: "You do not have permission to modify the default payment settings",
186-
});
183+
throw new ErrorWithCode(
184+
ErrorCode.Unauthorized,
185+
"You do not have permission to modify the default payment settings"
186+
);
187187
}
188188

189189
await this.permissionService.validatePermissions(input);

packages/features/ee/organizations/lib/OrganizationPermissionService.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import logger from "@calcom/lib/logger";
55
import { safeStringify } from "@calcom/lib/safeStringify";
66
import { prisma } from "@calcom/prisma";
77
import { MembershipRole } from "@calcom/prisma/enums";
8-
9-
import { TRPCError } from "@trpc/server";
8+
import { ErrorCode } from "@calcom/lib/errorCodes";
9+
import { ErrorWithCode } from "@calcom/lib/errors";
1010

1111
import type { OnboardingUser } from "./service/onboarding/types";
1212

@@ -102,36 +102,30 @@ export class OrganizationPermissionService {
102102
} & SeatsPrice
103103
): Promise<boolean> {
104104
if (!(await this.hasPermissionToCreateForEmail(input.orgOwnerEmail))) {
105-
throw new TRPCError({
106-
code: "UNAUTHORIZED",
107-
message: "you_do_not_have_permission_to_create_an_organization_for_this_email",
108-
});
105+
throw new ErrorWithCode(
106+
ErrorCode.Unauthorized,
107+
"you_do_not_have_permission_to_create_an_organization_for_this_email"
108+
);
109109
}
110110

111111
if (await this.hasConflictingOrganization({ slug: input.slug })) {
112-
throw new TRPCError({
113-
code: "BAD_REQUEST",
114-
message: "organization_already_exists_with_this_slug",
115-
});
112+
throw new ErrorWithCode(ErrorCode.BadRequest, "organization_already_exists_with_this_slug");
116113
}
117114

118115
if (await this.hasCompletedOnboarding(input.orgOwnerEmail)) {
119116
// TODO: Consider redirecting to success page
120-
throw new TRPCError({
121-
code: "BAD_REQUEST",
122-
message: "organization_onboarding_already_completed",
123-
});
117+
throw new ErrorWithCode(ErrorCode.BadRequest, "organization_onboarding_already_completed");
124118
}
125119

126120
const teamsToMigrate = input.teams
127121
?.filter((team) => team.id > 0 && team.isBeingMigrated)
128122
.map((team) => team.id);
129123

130124
if (teamsToMigrate && !(await this.hasPermissionToMigrateTeams(teamsToMigrate))) {
131-
throw new TRPCError({
132-
code: "UNAUTHORIZED",
133-
message: "you_do_not_have_permission_to_migrate_one_or_more_of_the_teams",
134-
});
125+
throw new ErrorWithCode(
126+
ErrorCode.Unauthorized,
127+
"you_do_not_have_permission_to_migrate_one_or_more_of_the_teams"
128+
);
135129
}
136130

137131
return true;

packages/features/ee/sso/lib/sso.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/
33
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
44
import type { PrismaClient } from "@calcom/prisma";
55
import { IdentityProvider } from "@calcom/prisma/enums";
6-
7-
import { TRPCError } from "@trpc/server";
6+
import { ErrorCode } from "@calcom/lib/errorCodes";
7+
import { ErrorWithCode } from "@calcom/lib/errors";
88

99
import jackson from "./jackson";
1010
import { tenantPrefix, samlProductID } from "./saml";
@@ -29,21 +29,13 @@ export const ssoTenantProduct = async (prisma: PrismaClient, email: string) => {
2929
let memberships = await getAllAcceptedMemberships({ prisma, email });
3030

3131
if (!memberships || memberships.length === 0) {
32-
if (!HOSTED_CAL_FEATURES)
33-
throw new TRPCError({
34-
code: "UNAUTHORIZED",
35-
message: "no_account_exists",
36-
});
32+
if (!HOSTED_CAL_FEATURES) throw new ErrorWithCode(ErrorCode.Unauthorized, "no_account_exists");
3733

3834
const domain = email.split("@")[1];
3935
const organizationRepository = getOrganizationRepository();
4036
const organization = await organizationRepository.getVerifiedOrganizationByAutoAcceptEmailDomain(domain);
4137

42-
if (!organization)
43-
throw new TRPCError({
44-
code: "UNAUTHORIZED",
45-
message: "no_account_exists",
46-
});
38+
if (!organization) throw new ErrorWithCode(ErrorCode.Unauthorized, "no_account_exists");
4739

4840
const createUsersAndConnectToOrgProps = {
4941
emailsToCreate: [email],
@@ -58,10 +50,7 @@ export const ssoTenantProduct = async (prisma: PrismaClient, email: string) => {
5850
memberships = await getAllAcceptedMemberships({ prisma, email });
5951

6052
if (!memberships || memberships.length === 0)
61-
throw new TRPCError({
62-
code: "UNAUTHORIZED",
63-
message: "no_account_exists",
64-
});
53+
throw new ErrorWithCode(ErrorCode.Unauthorized, "no_account_exists");
6554
}
6655

6756
// Check SSO connections for each team user is a member of
@@ -81,11 +70,10 @@ export const ssoTenantProduct = async (prisma: PrismaClient, email: string) => {
8170
.filter((connections) => connections.length > 0);
8271

8372
if (connectionsFound.length === 0) {
84-
throw new TRPCError({
85-
code: "BAD_REQUEST",
86-
message:
87-
"Could not find a SSO Identity Provider for your email. Please contact your admin to ensure you have been given access to Cal",
88-
});
73+
throw new ErrorWithCode(
74+
ErrorCode.BadRequest,
75+
"Could not find a SSO Identity Provider for your email. Please contact your admin to ensure you have been given access to Cal"
76+
);
8977
}
9078

9179
return {

packages/features/ee/teams/services/teamService.test.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepos
77
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
88
import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/createAProfileForAnExistingUser";
99
import { deleteDomain } from "@calcom/lib/domainManager/organization";
10+
import { ErrorCode } from "@calcom/lib/errorCodes";
11+
import { ErrorWithCode } from "@calcom/lib/errors";
1012
import type { Membership, Team, User, VerificationToken, Profile } from "@calcom/prisma/client";
1113
import { MembershipRole } from "@calcom/prisma/enums";
1214

13-
import { TRPCError } from "@trpc/server";
14-
1515
import { TeamService } from "./teamService";
1616

1717
vi.mock("@calcom/ee/billing/di/containers/Billing");
@@ -77,7 +77,7 @@ describe("TeamService", () => {
7777
describe("inviteMemberByToken", () => {
7878
it("should throw error if verification token is not found", async () => {
7979
prismaMock.verificationToken.findFirst.mockResolvedValue(null);
80-
await expect(TeamService.inviteMemberByToken("invalid-token", 1)).rejects.toThrow(TRPCError);
80+
await expect(TeamService.inviteMemberByToken("invalid-token", 1)).rejects.toThrow(ErrorWithCode);
8181
});
8282

8383
it("should create provisional membership and update billing", async () => {
@@ -235,7 +235,7 @@ describe("TeamService", () => {
235235
describe("acceptInvitationByToken", () => {
236236
it("should throw error if verification token is not found", async () => {
237237
prismaMock.verificationToken.findFirst.mockResolvedValue(null);
238-
await expect(TeamService.acceptInvitationByToken("invalid-token", 1)).rejects.toThrow(TRPCError);
238+
await expect(TeamService.acceptInvitationByToken("invalid-token", 1)).rejects.toThrow(ErrorWithCode);
239239
});
240240

241241
it("should throw error if token is not associated with team", async () => {
@@ -251,10 +251,7 @@ describe("TeamService", () => {
251251
);
252252

253253
await expect(TeamService.acceptInvitationByToken("valid-token", 1)).rejects.toThrow(
254-
new TRPCError({
255-
code: "NOT_FOUND",
256-
message: "Invite token is not associated with any team",
257-
})
254+
new ErrorWithCode(ErrorCode.NotFound, "Invite token is not associated with any team")
258255
);
259256
});
260257

@@ -272,7 +269,7 @@ describe("TeamService", () => {
272269
prismaMock.user.findUnique.mockResolvedValue(null);
273270

274271
await expect(TeamService.acceptInvitationByToken("valid-token", 1)).rejects.toThrow(
275-
new TRPCError({ code: "NOT_FOUND", message: "User not found" })
272+
new ErrorWithCode(ErrorCode.NotFound, "User not found")
276273
);
277274
});
278275

@@ -295,10 +292,7 @@ describe("TeamService", () => {
295292
prismaMock.user.findUnique.mockResolvedValue(mockUser as User);
296293

297294
await expect(TeamService.acceptInvitationByToken("valid-token", 1)).rejects.toThrow(
298-
new TRPCError({
299-
code: "FORBIDDEN",
300-
message: "This invitation is not for your account",
301-
})
295+
new ErrorWithCode(ErrorCode.Forbidden, "This invitation is not for your account")
302296
);
303297
});
304298

@@ -321,10 +315,7 @@ describe("TeamService", () => {
321315
prismaMock.user.findUnique.mockResolvedValue(mockUser as User);
322316

323317
await expect(TeamService.acceptInvitationByToken("valid-token", 1)).rejects.toThrow(
324-
new TRPCError({
325-
code: "FORBIDDEN",
326-
message: "This invitation is not for your account",
327-
})
318+
new ErrorWithCode(ErrorCode.Forbidden, "This invitation is not for your account")
328319
);
329320
});
330321

0 commit comments

Comments
 (0)