perf: remove trpc basecamp3 router (#23785)

* remove trpc basecamp3 router

* update

* define business logic in appstore package

* cleaner

* cleaner code

* refactor

* use zod

* cleaner

* update
This commit is contained in:
Benny Joo
2025-09-12 15:32:50 -03:00
committed by GitHub
parent a63802dea7
commit 68f2fc9174
7 changed files with 106 additions and 111 deletions
@@ -1,4 +0,0 @@
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import appBasecamp3 from "@calcom/trpc/server/routers/apps/basecamp3/_router";
export default createNextApiHandler(appBasecamp3);
@@ -1,47 +1,54 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { refreshAccessToken } from "@calcom/app-store/basecamp3/lib/helpers";
import type { BasecampToken } from "@calcom/app-store/basecamp3/lib/types";
import type { PrismaClient } from "@calcom/prisma/client";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import type { TProjectMutationInputSchema } from "./projectMutation.schema";
interface ProjectMutationHandlerOptions {
ctx: {
prisma: PrismaClient;
user: NonNullable<TrpcSessionUser>;
};
input: TProjectMutationInputSchema;
}
interface IDock {
id: number;
name: string;
}
export const projectMutationHandler = async ({ ctx, input }: ProjectMutationHandlerOptions) => {
const { user_agent } = await getAppKeysFromSlug("basecamp3");
const { user, prisma } = ctx;
const { projectId } = input;
const ZProjectMutationInputSchema = z.object({ projectId: z.string() });
async function handler(req: NextApiRequest) {
const userId = req.session?.user?.id;
if (!userId) {
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
}
const parsed = ZProjectMutationInputSchema.safeParse(req.body ?? {});
if (!parsed.success) {
throw new HttpError({
statusCode: 400,
message: "Invalid request body",
});
}
const { projectId } = parsed.data;
const { user_agent } = await getAppKeysFromSlug("basecamp3");
const credential = await prisma.credential.findFirst({
where: {
userId: user?.id,
},
where: { userId },
select: credentialForCalendarServiceSelect,
});
if (!credential) {
throw new TRPCError({ code: "FORBIDDEN", message: "No credential found for user" });
throw new HttpError({ statusCode: 403, message: "No credential found for user" });
}
let credentialKey = credential.key as BasecampToken;
if (credentialKey.expires_at < Date.now()) {
credentialKey = (await refreshAccessToken(credential)) as BasecampToken;
}
// get schedule id
const basecampUserId = credentialKey.account.id;
const scheduleResponse = await fetch(
`https://3.basecampapi.com/${basecampUserId}/projects/${projectId}.json`,
@@ -52,12 +59,22 @@ export const projectMutationHandler = async ({ ctx, input }: ProjectMutationHand
},
}
);
if (!scheduleResponse.ok) {
throw new HttpError({ statusCode: 400, message: "Failed to fetch project details" });
}
const scheduleJson = await scheduleResponse.json();
const scheduleId = scheduleJson.dock.find((dock: IDock) => dock.name === "schedule").id;
await prisma.credential.update({
where: { id: credential.id },
data: { key: { ...credentialKey, projectId: Number(projectId), scheduleId } },
});
return { message: "Updated project successfully" };
};
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -1,34 +1,35 @@
import type { NextApiRequest } from "next";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { refreshAccessToken } from "@calcom/app-store/basecamp3/lib/helpers";
import type { BasecampToken } from "@calcom/app-store/basecamp3/lib/types";
import type { PrismaClient } from "@calcom/prisma/client";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { HttpError } from "@calcom/lib/http-error";
import { TRPCError } from "@trpc/server";
async function handler(req: NextApiRequest) {
const userId = req.session?.user?.id;
if (!userId) {
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
}
interface ProjectsHandlerOptions {
ctx: {
prisma: PrismaClient;
user: NonNullable<TrpcSessionUser>;
};
}
export const projectHandler = async ({ ctx }: ProjectsHandlerOptions) => {
const { user_agent } = await getAppKeysFromSlug("basecamp3");
const { user, prisma } = ctx;
const credential = await prisma.credential.findFirst({
where: {
userId: user?.id,
},
where: { userId },
select: credentialForCalendarServiceSelect,
});
if (!credential) {
throw new TRPCError({ code: "FORBIDDEN", message: "No credential found for user" });
throw new HttpError({ statusCode: 403, message: "No credential found for user" });
}
let credentialKey = credential.key as BasecampToken;
if (!credentialKey.account) {
return;
return { currentProject: null, projects: [] };
}
if (credentialKey.expires_at < Date.now()) {
@@ -36,10 +37,18 @@ export const projectHandler = async ({ ctx }: ProjectsHandlerOptions) => {
}
const url = `${credentialKey.account.href}/projects.json`;
const resp = await fetch(url, {
headers: { "User-Agent": user_agent as string, Authorization: `Bearer ${credentialKey.access_token}` },
});
if (!resp.ok) {
throw new HttpError({ statusCode: 400, message: "Failed to fetch Basecamp projects" });
}
const projects = await resp.json();
return { currentProject: credentialKey.projectId, projects };
};
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -1,34 +1,47 @@
import { useState, useEffect } from "react";
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
import { trpc } from "@calcom/trpc/react";
import { Select } from "@calcom/ui/components/form";
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({}) => {
const [projects, setProjects] = useState();
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
const { data } = trpc.viewer.appBasecamp3.projects.useQuery();
const setProject = trpc.viewer.appBasecamp3.projectMutation.useMutation();
type Basecamp3Project = { label: string; value: string };
useEffect(
function refactorMeWithoutEffect() {
setSelectedProject({
value: data?.projects.currentProject,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: data?.projects?.find((project: any) => project.id === data?.currentProject)?.name,
});
setProjects(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?.projects?.map((project: any) => {
return {
value: project.id,
label: project.name,
};
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = () => {
const [projects, setProjects] = useState<Basecamp3Project[] | undefined>();
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
useEffect(() => {
async function loadProjects() {
const res = await fetch("/api/integrations/basecamp3/projects");
if (!res.ok) return;
const json = await res.json();
const currentProjectId = json?.currentProject;
const options: Basecamp3Project[] = json?.projects?.map(
(project: { id: number | string; name: string }) => ({
value: String(project.id),
label: project.name,
})
);
},
[data]
);
if (!options) return;
setProjects(options);
if (currentProjectId) {
const match = options.find((project: Basecamp3Project) => project.value === String(currentProjectId));
if (match) setSelectedProject(match);
}
}
loadProjects();
}, []);
async function handleProjectChange(project: { label: string; value: string } | null) {
if (!project) return;
await fetch("/api/integrations/basecamp3/projectMutation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ projectId: project.value }),
});
setSelectedProject(project);
}
return (
<div className="mt-2 text-sm">
@@ -41,12 +54,7 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({}) => {
options={projects}
isLoading={!projects}
className="md:min-w-[120px]"
onChange={(project) => {
if (project) {
setProject.mutate({ projectId: project?.value.toString() });
setSelectedProject(project);
}
}}
onChange={handleProjectChange}
value={selectedProject}
/>
</div>
@@ -1,26 +0,0 @@
import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure";
import { router } from "@calcom/trpc/server/trpc";
import { ZProjectMutationInputSchema } from "./projectMutation.schema";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const UNSTABLE_HANDLER_CACHE: any = {};
const appBasecamp3 = router({
projects: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.projects) {
UNSTABLE_HANDLER_CACHE.projects = await import("./projects.handler").then((mod) => mod.projectHandler);
}
return UNSTABLE_HANDLER_CACHE.projects({ ctx });
}),
projectMutation: authedProcedure.input(ZProjectMutationInputSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.projectMutation) {
UNSTABLE_HANDLER_CACHE.projectMutation = await import("./projectMutation.handler").then(
(mod) => mod.projectMutationHandler
);
}
return UNSTABLE_HANDLER_CACHE.projectMutation({ ctx, input });
}),
});
export default appBasecamp3;
@@ -1,7 +0,0 @@
import { z } from "zod";
export const ZProjectMutationInputSchema = z.object({
projectId: z.string(),
});
export type TProjectMutationInputSchema = z.infer<typeof ZProjectMutationInputSchema>;
@@ -3,7 +3,6 @@ import { featureFlagRouter } from "@calcom/features/flags/server/router";
import { insightsRouter } from "@calcom/features/insights/server/trpc-router";
import { router, mergeRouters } from "../../trpc";
import app_Basecamp3 from "../apps/basecamp3/_router";
import app_RoutingForms from "../apps/routing-forms/_router";
import { loggedInViewerRouter } from "../loggedInViewer/_router";
import { publicViewerRouter } from "../publicViewer/_router";
@@ -79,7 +78,6 @@ export const viewerRouter = router({
// NOTE: Add all app related routes in the bottom till the problem described in @calcom/app-store/trpc-routers.ts is solved.
// After that there would just one merge call here for all the apps.
appRoutingForms: app_RoutingForms,
appBasecamp3: app_Basecamp3,
features: featureFlagRouter,
users: userAdminRouter,
oAuth: oAuthRouter,