refactor: Move insights trpc router from features package to trpc layer (#25778)

* update imports

* move trpc router from features to trpc

* update imports

* wip

* update imports

* wip

* wip

* wip

* fix import

* create objectToCsv.ts

* rename

* fix: restore proper types and static imports in insights handlers

- Add proper type definitions for all handler options instead of using 'any'
- Restore static imports for PermissionCheckService and MembershipRole
- Remove dynamic imports that were introduced during refactoring

This ensures the refactoring is pure with no behavioral changes.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update getEventTypeList import path after utils.ts deletion

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* remove handlers

* fix

* refactor

* wip

* rm

* wip

* select only needed + fix type error

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
This commit is contained in:
authored and GitHub committed 2025-12-14 08:44:36 -03:00
1 parent 9ef650ceed
commit 43b6c63294
8 files changed
+458 -445

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
import { insightsRouter } from "@calcom/features/insights/server/trpc-router";
import { insightsRouter } from "@calcom/trpc/server/routers/viewer/insights/_router";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
export default createNextApiHandler(insightsRouter);
@@ -0,0 +1,205 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
import { readonlyPrisma } from "@calcom/prisma";
vi.mock("@calcom/prisma", () => ({
readonlyPrisma: {
eventType: {
findMany: vi.fn(),
},
team: {
findMany: vi.fn(),
},
membership: {
findFirst: vi.fn(),
},
},
}));
describe("EventTypeRepository", () => {
let eventTypeRepository: EventTypeRepository;
beforeEach(() => {
vi.resetAllMocks();
eventTypeRepository = new EventTypeRepository(readonlyPrisma);
});
const mockUser = {
id: 1,
organizationId: 10,
isOwnerAdminOfParentTeam: false,
};
const mockEventTypes = [
{
id: 1,
slug: "personal-event",
title: "Personal Event",
teamId: null,
userId: 1,
team: null,
},
{
id: 2,
slug: "team-event",
title: "Team Event",
teamId: 5,
userId: null,
team: { name: "Team A" },
},
];
describe("getEventTypeList", () => {
describe("Early return scenarios", () => {
it("should return empty array when no teamId, userId, or isAll provided", async () => {
const result = await eventTypeRepository.getEventTypeList({
teamId: null,
userId: null,
isAll: false,
user: mockUser,
});
expect(result).toEqual([]);
expect(readonlyPrisma.eventType.findMany).not.toHaveBeenCalled();
});
});
describe("Personal events filtering", () => {
it("should return only user's personal events when userId provided", async () => {
const personalEvents = [mockEventTypes[0]];
vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue(personalEvents);
const result = await eventTypeRepository.getEventTypeList({
teamId: null,
userId: 1,
isAll: false,
user: mockUser,
});
expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
userId: mockUser.id,
teamId: null,
},
});
expect(result).toEqual(personalEvents);
});
});
describe("Organization-wide view (isAll = true)", () => {
it("should return team events and user's personal events for owner/admin", async () => {
const childTeams = [{ id: 11 }, { id: 12 }];
const allEvents = [...mockEventTypes];
vi.mocked(readonlyPrisma.team.findMany).mockResolvedValue(childTeams);
vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue(allEvents);
const ownerUser = { ...mockUser, isOwnerAdminOfParentTeam: true };
const result = await eventTypeRepository.getEventTypeList({
teamId: null,
userId: null,
isAll: true,
user: ownerUser,
});
expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
OR: [
{
teamId: {
in: [10, 11, 12],
},
},
{
userId: ownerUser.id,
teamId: null,
},
],
},
});
expect(result).toEqual(allEvents);
});
});
describe("Team-specific view", () => {
it("should return team events for team members", async () => {
const membership = { teamId: 5, userId: 1, role: "MEMBER" };
vi.mocked(readonlyPrisma.membership.findFirst).mockResolvedValue(membership);
vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue([mockEventTypes[1]]);
const result = await eventTypeRepository.getEventTypeList({
teamId: 5,
userId: null,
isAll: false,
user: mockUser,
});
expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
teamId: 5,
OR: [{ userId: mockUser.id }, { users: { some: { id: mockUser.id } } }],
},
});
expect(result).toEqual([mockEventTypes[1]]);
});
it("should throw error when user is not part of team and not owner/admin", async () => {
vi.mocked(readonlyPrisma.membership.findFirst).mockResolvedValue(null);
await expect(
eventTypeRepository.getEventTypeList({
teamId: 5,
userId: null,
isAll: false,
user: mockUser,
})
).rejects.toThrow("User is not part of a team/org");
});
});
});
// TODO: Add tests for other EventTypeRepository methods as they are added
// Examples:
// - describe("findById", () => { ... })
// - describe("create", () => { ... })
// - describe("findAllByUpId", () => { ... })
// etc.
});
@@ -1693,4 +1693,145 @@ export class EventTypeRepository {
},
});
}
async getEventTypeList({
teamId,
userId,
isAll,
user,
}: {
teamId: number | null | undefined;
userId: number | null | undefined;
isAll: boolean | undefined;
user: {
id: number;
organizationId: number | null;
isOwnerAdminOfParentTeam: boolean;
};
}) {
if (!teamId && !userId && !isAll) {
return [];
}
const membershipWhereConditional: Prisma.MembershipWhereInput = {};
let childrenTeamIds: number[] = [];
if (userId && !teamId && !isAll) {
const eventTypeResult = await this.prismaClient.eventType.findMany({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
userId: user.id,
teamId: null,
},
});
return eventTypeResult;
}
if (isAll && user.organizationId && user.isOwnerAdminOfParentTeam) {
const childTeams = await this.prismaClient.team.findMany({
where: {
parentId: user.organizationId,
},
select: {
id: true,
},
});
if (childTeams.length > 0) {
childrenTeamIds = childTeams.map((team) => team.id);
}
const eventTypeResult = await this.prismaClient.eventType.findMany({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
OR: [
{
teamId: {
in: [user.organizationId, ...childrenTeamIds],
},
},
{
userId: user.id,
teamId: null,
},
],
},
});
return eventTypeResult;
}
if (teamId && !isAll) {
membershipWhereConditional["teamId"] = teamId;
membershipWhereConditional["userId"] = user.id;
}
// I'm not using unique here since when userId comes from input we should look for every
// event type that user owns
const membership = await this.prismaClient.membership.findFirst({
where: membershipWhereConditional,
});
if (!membership && !user.isOwnerAdminOfParentTeam) {
throw new Error("User is not part of a team/org");
}
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {};
if (teamId && !isAll) {
eventTypeWhereConditional["teamId"] = teamId;
}
let isMember = membership?.role === "MEMBER";
if (user.isOwnerAdminOfParentTeam) {
isMember = false;
}
if (isMember) {
eventTypeWhereConditional["OR"] = [{ userId: user.id }, { users: { some: { id: user.id } } }];
// @TODO this is not working as expected
// hosts: { some: { id: user.id } },
}
const eventTypeResult = await this.prismaClient.eventType.findMany({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: eventTypeWhereConditional,
});
return eventTypeResult;
}
}
@@ -0,0 +1,21 @@
export function objectToCsv(data: Record<string, string>[]) {
if (!data.length) return "";
const headers = Object.keys(data[0]);
const csvRows = [
headers.join(","),
...data.map((row) =>
headers
.map((header) => {
const value = row[header]?.toString() || "";
// Escape quotes and wrap in quotes if contains comma or newline
return value.includes(",") || value.includes("\n") || value.includes('"')
? `"${value.replace(/"/g, '""')}"` // escape double quotes
: value;
})
.join(",")
),
];
return csvRows.join("\n");
}
@@ -1,199 +0,0 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { readonlyPrisma } from "@calcom/prisma";
import { getEventTypeList } from "../trpc-router";
vi.mock("@calcom/prisma", () => ({
readonlyPrisma: {
eventType: {
findMany: vi.fn(),
},
team: {
findMany: vi.fn(),
},
membership: {
findFirst: vi.fn(),
},
},
}));
describe("getEventTypeList", () => {
beforeEach(() => {
vi.resetAllMocks();
});
const mockUser = {
id: 1,
organizationId: 10,
isOwnerAdminOfParentTeam: false,
};
const mockEventTypes = [
{
id: 1,
slug: "personal-event",
title: "Personal Event",
teamId: null,
userId: 1,
team: null,
},
{
id: 2,
slug: "team-event",
title: "Team Event",
teamId: 5,
userId: null,
team: { name: "Team A" },
},
];
describe("Early return scenarios", () => {
it("should return empty array when no teamId, userId, or isAll provided", async () => {
const result = await getEventTypeList({
prisma: readonlyPrisma,
teamId: null,
userId: null,
isAll: false,
user: mockUser,
});
expect(result).toEqual([]);
expect(readonlyPrisma.eventType.findMany).not.toHaveBeenCalled();
});
});
describe("Personal events filtering", () => {
it("should return only user's personal events when userId provided", async () => {
const personalEvents = [mockEventTypes[0]];
vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue(personalEvents);
const result = await getEventTypeList({
prisma: readonlyPrisma,
teamId: null,
userId: 1,
isAll: false,
user: mockUser,
});
expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
userId: mockUser.id,
teamId: null,
},
});
expect(result).toEqual(personalEvents);
});
});
describe("Organization-wide view (isAll = true)", () => {
it("should return team events and user's personal events for owner/admin", async () => {
const childTeams = [{ id: 11 }, { id: 12 }];
const allEvents = [...mockEventTypes];
vi.mocked(readonlyPrisma.team.findMany).mockResolvedValue(childTeams);
vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue(allEvents);
const ownerUser = { ...mockUser, isOwnerAdminOfParentTeam: true };
const result = await getEventTypeList({
prisma: readonlyPrisma,
teamId: null,
userId: null,
isAll: true,
user: ownerUser,
});
expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
OR: [
{
teamId: {
in: [10, 11, 12],
},
},
{
userId: ownerUser.id,
teamId: null,
},
],
},
});
expect(result).toEqual(allEvents);
});
});
describe("Team-specific view", () => {
it("should return team events for team members", async () => {
const membership = { teamId: 5, userId: 1, role: "MEMBER" };
vi.mocked(readonlyPrisma.membership.findFirst).mockResolvedValue(membership);
vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue([mockEventTypes[1]]);
const result = await getEventTypeList({
prisma: readonlyPrisma,
teamId: 5,
userId: null,
isAll: false,
user: mockUser,
});
expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
teamId: 5,
OR: [{ userId: mockUser.id }, { users: { some: { id: mockUser.id } } }],
},
});
expect(result).toEqual([mockEventTypes[1]]);
});
it("should throw error when user is not part of team and not owner/admin", async () => {
vi.mocked(readonlyPrisma.membership.findFirst).mockResolvedValue(null);
await expect(
getEventTypeList({
prisma: readonlyPrisma,
teamId: 5,
userId: null,
isAll: false,
user: mockUser,
})
).rejects.toThrow("User is not part of a team/org");
});
});
});
@@ -1,6 +1,5 @@
import { userAdminRouter } from "@calcom/features/ee/users/server/trpc-router";
import { featureFlagRouter } from "@calcom/features/flags/server/router";
import { insightsRouter } from "@calcom/features/insights/server/trpc-router";
import { router } from "../../trpc";
import app_RoutingForms from "../apps/routing-forms/_router";
@@ -27,6 +26,7 @@ import { eventTypesRouter as heavyEventTypesRouter } from "./eventTypes/heavy/_r
import { filterSegmentsRouter } from "./filterSegments/_router";
import { googleWorkspaceRouter } from "./googleWorkspace/_router";
import { i18nRouter } from "./i18n/_router";
import { insightsRouter } from "./insights/_router";
import { meRouter } from "./me/_router";
import { oAuthRouter } from "./oAuth/_router";
import { oooRouter } from "./ooo/_router";
@@ -3,92 +3,36 @@ import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { getInsightsBookingService } from "@calcom/features/di/containers/InsightsBooking";
import { getInsightsRoutingService } from "@calcom/features/di/containers/InsightsRouting";
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
import {
extractDateRangeFromColumnFilters,
replaceDateRangeColumnFilter,
} from "@calcom/features/insights/lib/bookingUtils";
import { objectToCsv } from "@calcom/features/insights/lib/objectToCsv";
import {
getTimeView,
getDateRanges,
type GetDateRangesParams,
} from "@calcom/features/insights/server/insightsDateUtils";
import {
bookingRepositoryBaseInputSchema,
insightsRoutingServiceInputSchema,
insightsRoutingServicePaginatedInputSchema,
routingRepositoryBaseInputSchema,
routedToPerPeriodInputSchema,
routedToPerPeriodCsvInputSchema,
bookingRepositoryBaseInputSchema,
} from "@calcom/features/insights/server/raw-data.schema";
import { RoutingEventsInsights } from "@calcom/features/insights/server/routing-events";
import { VirtualQueuesInsights } from "@calcom/features/insights/server/virtual-queues";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import type { PrismaClient } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure";
import { router } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import { getTimeView, getDateRanges, type GetDateRangesParams } from "./insightsDateUtils";
import { RoutingEventsInsights } from "./routing-events";
import { VirtualQueuesInsights } from "./virtual-queues";
const UserBelongsToTeamInput = z.object({
teamId: z.coerce.number().optional().nullable(),
isAll: z.boolean().optional(),
});
const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, getRawInput }) => {
const parse = UserBelongsToTeamInput.safeParse(await getRawInput());
if (!parse.success) {
throw new TRPCError({ code: "BAD_REQUEST" });
}
// If teamId is provided, check if user belongs to team
// If teamId is not provided, check if user belongs to any team
const membershipWhereConditional: Prisma.MembershipWhereInput = {
userId: ctx.user.id,
accepted: true,
};
if (parse.data.teamId) {
membershipWhereConditional["teamId"] = parse.data.teamId;
}
const membership = await ctx.insightsDb.membership.findFirst({
where: membershipWhereConditional,
});
let isOwnerAdminOfParentTeam = false;
// Probably we couldn't find a membership because the user is not a direct member of the team
// So that would mean ctx.user.organization is present
if ((parse.data.isAll && ctx.user.organizationId) || (!membership && ctx.user.organizationId)) {
//Look for membership type in organizationId
if (!membership && ctx.user.organizationId && parse.data.teamId) {
const isChildTeamOfOrg = await ctx.insightsDb.team.findFirst({
where: {
id: parse.data.teamId,
parentId: ctx.user.organizationId,
},
});
if (!isChildTeamOfOrg) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
const hasOrgAccess = await checkInsightsPermission(ctx.user.id, ctx.user.organizationId);
if (!hasOrgAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
isOwnerAdminOfParentTeam = true;
}
return next({
ctx: {
user: {
...ctx.user,
isOwnerAdminOfParentTeam,
},
},
});
});
import { userBelongsToTeamProcedure } from "./procedures/userBelongsToTeam";
const userSelect = {
id: true,
@@ -98,16 +42,6 @@ const userSelect = {
avatarUrl: true,
};
async function checkInsightsPermission(userId: number, teamId: number): Promise<boolean> {
const permissionCheckService = new PermissionCheckService();
return await permissionCheckService.checkPermission({
userId,
teamId,
permission: "insights.read",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
}
const emptyResponseBookingKPIStats = {
empty: true,
created: {
@@ -633,8 +567,8 @@ export const insightsRouter = router({
const { prisma, user } = ctx;
const { teamId, userId, isAll } = input;
const eventTypeList = await getEventTypeList({
prisma,
const eventTypeRepository = new EventTypeRepository(prisma);
const eventTypeList = await eventTypeRepository.getEventTypeList({
teamId,
userId,
isAll,
@@ -944,168 +878,3 @@ export const insightsRouter = router({
}
}),
});
export async function getEventTypeList({
prisma,
teamId,
userId,
isAll,
user,
}: {
prisma: PrismaClient;
teamId: number | null | undefined;
userId: number | null | undefined;
isAll: boolean | undefined;
user: {
id: number;
organizationId: number | null;
isOwnerAdminOfParentTeam: boolean;
};
}) {
if (!teamId && !userId && !isAll) {
return [];
}
const membershipWhereConditional: Prisma.MembershipWhereInput = {};
let childrenTeamIds: number[] = [];
if (userId && !teamId && !isAll) {
const eventTypeResult = await prisma.eventType.findMany({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
userId: user.id,
teamId: null,
},
});
return eventTypeResult;
}
if (isAll && user.organizationId && user.isOwnerAdminOfParentTeam) {
const childTeams = await prisma.team.findMany({
where: {
parentId: user.organizationId,
},
select: {
id: true,
},
});
if (childTeams.length > 0) {
childrenTeamIds = childTeams.map((team) => team.id);
}
const eventTypeResult = await prisma.eventType.findMany({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: {
OR: [
{
teamId: {
in: [user.organizationId, ...childrenTeamIds],
},
},
{
userId: user.id,
teamId: null,
},
],
},
});
return eventTypeResult;
}
if (teamId && !isAll) {
membershipWhereConditional["teamId"] = teamId;
membershipWhereConditional["userId"] = user.id;
}
// I'm not using unique here since when userId comes from input we should look for every
// event type that user owns
const membership = await prisma.membership.findFirst({
where: membershipWhereConditional,
});
if (!membership && !user.isOwnerAdminOfParentTeam) {
throw new Error("User is not part of a team/org");
}
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {};
if (teamId && !isAll) {
eventTypeWhereConditional["teamId"] = teamId;
}
let isMember = membership?.role === "MEMBER";
if (user.isOwnerAdminOfParentTeam) {
isMember = false;
}
if (isMember) {
eventTypeWhereConditional["OR"] = [{ userId: user.id }, { users: { some: { id: user.id } } }];
// @TODO this is not working as expected
// hosts: { some: { id: user.id } },
}
const eventTypeResult = await prisma.eventType.findMany({
select: {
id: true,
slug: true,
title: true,
teamId: true,
userId: true,
team: {
select: {
name: true,
},
},
},
where: eventTypeWhereConditional,
});
return eventTypeResult;
}
function objectToCsv(data: Record<string, string>[]) {
if (!data.length) return "";
const headers = Object.keys(data[0]);
const csvRows = [
headers.join(","),
...data.map((row) =>
headers
.map((header) => {
const value = row[header]?.toString() || "";
// Escape quotes and wrap in quotes if contains comma or newline
return value.includes(",") || value.includes("\n") || value.includes('"')
? `"${value.replace(/"/g, '""')}"` // escape double quotes
: value;
})
.join(",")
),
];
return csvRows.join("\n");
}
@@ -0,0 +1,76 @@
import { z } from "zod";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { MembershipRole } from "@calcom/prisma/enums";
import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure";
import { TRPCError } from "@trpc/server";
const UserBelongsToTeamInput = z.object({
teamId: z.coerce.number().optional().nullable(),
isAll: z.boolean().optional(),
});
async function checkInsightsPermission(userId: number, teamId: number): Promise<boolean> {
const permissionCheckService = new PermissionCheckService();
return await permissionCheckService.checkPermission({
userId,
teamId,
permission: "insights.read",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
}
export const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, getRawInput }) => {
const parse = UserBelongsToTeamInput.safeParse(await getRawInput());
if (!parse.success) {
throw new TRPCError({ code: "BAD_REQUEST" });
}
// If teamId is provided, check if user belongs to team
// If teamId is not provided, check if user belongs to any team
const membership = await ctx.insightsDb.membership.findFirst({
where: {
userId: ctx.user.id,
accepted: true,
...(parse.data.teamId && { teamId: parse.data.teamId }),
},
select: { id: true },
});
let isOwnerAdminOfParentTeam = false;
// Probably we couldn't find a membership because the user is not a direct member of the team
// So that would mean ctx.user.organization is present
if ((parse.data.isAll && ctx.user.organizationId) || (!membership && ctx.user.organizationId)) {
//Look for membership type in organizationId
if (!membership && ctx.user.organizationId && parse.data.teamId) {
const isChildTeamOfOrg = await ctx.insightsDb.team.findFirst({
where: {
id: parse.data.teamId,
parentId: ctx.user.organizationId,
},
select: { id: true },
});
if (!isChildTeamOfOrg) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
const hasOrgAccess = await checkInsightsPermission(ctx.user.id, ctx.user.organizationId);
if (!hasOrgAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
isOwnerAdminOfParentTeam = true;
}
return next({
ctx: {
user: {
...ctx.user,
isOwnerAdminOfParentTeam,
},
},
});
});