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:
8 files changed
+458
-445
No files matched your search
@@ -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";
|
||||
|
||||
+13
-244
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user