refactor: convert TeamRepository to use dependency injection pattern (#22383)
* refactor: convert TeamRepository to use dependency injection pattern - Add constructor injection for PrismaClient in TeamRepository - Convert all static methods to instance methods using this.prismaClient - Update all usage sites to use two-step pattern: const teamRepo = new TeamRepository(prisma); teamRepo.method(...) - Optimize instance reuse within same function scopes where possible - Update test files to work with new instance pattern - Preserve all existing functionality and method signatures Follows the same dependency injection pattern as UserRepository refactoring Co-Authored-By: morgan@cal.com <morgan@cal.com> * chore: bump platform libs --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: morgan@cal.com <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
co-authored by
morgan@cal.com
Morgan
parent
7d02d1bb23
commit
efd15c5a3e
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.252",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.253",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { unstable_cache } from "next/cache";
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
import { TeamService } from "@calcom/lib/server/service/team";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -14,7 +15,8 @@ import { TeamsCTA } from "./CTA";
|
||||
|
||||
const getCachedTeams = unstable_cache(
|
||||
async (userId: number) => {
|
||||
return await TeamRepository.findTeamsByUserId({
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
return await teamRepo.findTeamsByUserId({
|
||||
userId,
|
||||
includeOrgs: true,
|
||||
});
|
||||
|
||||
@@ -388,10 +388,13 @@ describe("CreditService", () => {
|
||||
|
||||
describe("getMonthlyCredits", () => {
|
||||
it("should return 0 if subscription is not active", async () => {
|
||||
vi.mocked(TeamRepository.findTeamWithMembers).mockResolvedValue({
|
||||
id: 1,
|
||||
members: [{ accepted: true }],
|
||||
} as any);
|
||||
const mockTeamRepo = {
|
||||
findTeamWithMembers: vi.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
members: [{ accepted: true }],
|
||||
}),
|
||||
};
|
||||
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);
|
||||
|
||||
const mockTeamBillingService = {
|
||||
getSubscriptionStatus: vi.fn().mockResolvedValue("trialing"),
|
||||
@@ -405,10 +408,13 @@ describe("CreditService", () => {
|
||||
});
|
||||
|
||||
it("should calculate credits based on active members and price", async () => {
|
||||
vi.mocked(TeamRepository.findTeamWithMembers).mockResolvedValue({
|
||||
id: 1,
|
||||
members: [{ accepted: true }, { accepted: true }, { accepted: true }],
|
||||
} as any);
|
||||
const mockTeamRepo = {
|
||||
findTeamWithMembers: vi.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
members: [{ accepted: true }, { accepted: true }, { accepted: true }],
|
||||
}),
|
||||
};
|
||||
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);
|
||||
|
||||
const mockTeamBillingService = {
|
||||
getSubscriptionStatus: vi.fn().mockResolvedValue("active"),
|
||||
|
||||
@@ -532,7 +532,8 @@ export class CreditService {
|
||||
}
|
||||
|
||||
async getMonthlyCredits(teamId: number) {
|
||||
const team = await TeamRepository.findTeamWithMembers(teamId);
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
const team = await teamRepo.findTeamWithMembers(teamId);
|
||||
|
||||
if (!team) return 0;
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ export const createAProfileForAnExistingUser = async ({
|
||||
};
|
||||
organizationId: number;
|
||||
}) => {
|
||||
const org = await TeamRepository.findById({ id: organizationId });
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
const org = await teamRepo.findById({ id: organizationId });
|
||||
if (!org) {
|
||||
throw new Error(`Organization with id ${organizationId} not found`);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { TeamRepository } from "./team";
|
||||
|
||||
describe("TeamRepository", () => {
|
||||
let teamRepository: TeamRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
teamRepository = new TeamRepository(prismaMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -17,7 +20,7 @@ describe("TeamRepository", () => {
|
||||
describe("findById", () => {
|
||||
it("should return null if team is not found", async () => {
|
||||
prismaMock.team.findUnique.mockResolvedValue(null);
|
||||
const result = await TeamRepository.findById({ id: 1 });
|
||||
const result = await teamRepository.findById({ id: 1 });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -37,7 +40,7 @@ describe("TeamRepository", () => {
|
||||
requestedSlug: null,
|
||||
};
|
||||
prismaMock.team.findUnique.mockResolvedValue(mockTeam as unknown as Team);
|
||||
const result = await TeamRepository.findById({ id: 1 });
|
||||
const result = await teamRepository.findById({ id: 1 });
|
||||
expect(result).toEqual(mockTeam);
|
||||
});
|
||||
});
|
||||
@@ -67,7 +70,7 @@ describe("TeamRepository", () => {
|
||||
return callback(mockTx);
|
||||
});
|
||||
|
||||
const result = await TeamRepository.deleteById({ id: 1 });
|
||||
const result = await teamRepository.deleteById({ id: 1 });
|
||||
|
||||
expect(deleteManyEventTypeMock).toHaveBeenCalledWith({
|
||||
where: {
|
||||
@@ -93,7 +96,7 @@ describe("TeamRepository", () => {
|
||||
it("should return all teams with given parentId", async () => {
|
||||
const mockTeams = [{ id: 1 }, { id: 2 }];
|
||||
prismaMock.team.findMany.mockResolvedValue(mockTeams as unknown as Team[]);
|
||||
const result = await TeamRepository.findAllByParentId({ parentId: 1 });
|
||||
const result = await teamRepository.findAllByParentId({ parentId: 1 });
|
||||
expect(prismaMock.team.findMany).toHaveBeenCalledWith({
|
||||
where: { parentId: 1 },
|
||||
select: {
|
||||
@@ -116,7 +119,7 @@ describe("TeamRepository", () => {
|
||||
it("should return team with its members", async () => {
|
||||
const mockTeam = { id: 1, members: [] };
|
||||
prismaMock.team.findUnique.mockResolvedValue(mockTeam as unknown as Team & { members: [] });
|
||||
const result = await TeamRepository.findTeamWithMembers(1);
|
||||
const result = await teamRepository.findTeamWithMembers(1);
|
||||
expect(prismaMock.team.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
select: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { z } from "zod";
|
||||
|
||||
import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -166,8 +167,10 @@ const teamSelect = {
|
||||
} satisfies Prisma.TeamSelect;
|
||||
|
||||
export class TeamRepository {
|
||||
static async findById({ id }: { id: number }) {
|
||||
const team = await prisma.team.findUnique({
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async findById({ id }: { id: number }) {
|
||||
const team = await this.prismaClient.team.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -179,14 +182,14 @@ export class TeamRepository {
|
||||
return getParsedTeam(team);
|
||||
}
|
||||
|
||||
static async findAllByParentId({
|
||||
async findAllByParentId({
|
||||
parentId,
|
||||
select = teamSelect,
|
||||
}: {
|
||||
parentId: number;
|
||||
select?: Prisma.TeamSelect;
|
||||
}) {
|
||||
return await prisma.team.findMany({
|
||||
return await this.prismaClient.team.findMany({
|
||||
where: {
|
||||
parentId,
|
||||
},
|
||||
@@ -194,7 +197,7 @@ export class TeamRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findByIdAndParentId({
|
||||
async findByIdAndParentId({
|
||||
id,
|
||||
parentId,
|
||||
select = teamSelect,
|
||||
@@ -203,7 +206,7 @@ export class TeamRepository {
|
||||
parentId: number;
|
||||
select?: Prisma.TeamSelect;
|
||||
}) {
|
||||
return await prisma.team.findFirst({
|
||||
return await this.prismaClient.team.findFirst({
|
||||
where: {
|
||||
id,
|
||||
parentId,
|
||||
@@ -212,7 +215,7 @@ export class TeamRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findFirstBySlugAndParentSlug({
|
||||
async findFirstBySlugAndParentSlug({
|
||||
slug,
|
||||
parentSlug,
|
||||
select = teamSelect,
|
||||
@@ -221,7 +224,7 @@ export class TeamRepository {
|
||||
parentSlug: string | null;
|
||||
select?: Prisma.TeamSelect;
|
||||
}) {
|
||||
return await prisma.team.findFirst({
|
||||
return await this.prismaClient.team.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
parent: parentSlug ? whereClauseForOrgWithSlugOrRequestedSlug(parentSlug) : null,
|
||||
@@ -230,8 +233,8 @@ export class TeamRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async deleteById({ id }: { id: number }) {
|
||||
const deletedTeam = await prisma.$transaction(async (tx) => {
|
||||
async deleteById({ id }: { id: number }) {
|
||||
const deletedTeam = await this.prismaClient.$transaction(async (tx) => {
|
||||
await tx.eventType.deleteMany({
|
||||
where: {
|
||||
teamId: id,
|
||||
@@ -258,8 +261,8 @@ export class TeamRepository {
|
||||
return deletedTeam;
|
||||
}
|
||||
|
||||
static async findTeamWithMembers(teamId: number) {
|
||||
return await prisma.team.findUnique({
|
||||
async findTeamWithMembers(teamId: number) {
|
||||
return await this.prismaClient.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: {
|
||||
members: {
|
||||
@@ -275,8 +278,8 @@ export class TeamRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findTeamsByUserId({ userId, includeOrgs }: { userId: number; includeOrgs?: boolean }) {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
async findTeamsByUserId({ userId, includeOrgs }: { userId: number; includeOrgs?: boolean }) {
|
||||
const memberships = await this.prismaClient.membership.findMany({
|
||||
where: {
|
||||
// Show all the teams this user belongs to regardless of the team being part of the user's org or not
|
||||
// We don't want to restrict in the listing here. If we need to restrict a situation where a user is part of the org along with being part of a non-org team, we should do that instead of filtering out from here
|
||||
@@ -317,8 +320,8 @@ export class TeamRepository {
|
||||
}));
|
||||
}
|
||||
|
||||
static async findTeamWithOrganizationSettings(teamId: number) {
|
||||
return await prisma.team.findUnique({
|
||||
async findTeamWithOrganizationSettings(teamId: number) {
|
||||
return await this.prismaClient.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: {
|
||||
parent: {
|
||||
|
||||
@@ -35,14 +35,17 @@ const mockTeamBilling = {
|
||||
};
|
||||
|
||||
// Mock implementations that modify the in-memory database
|
||||
vi.mocked(TeamRepository.deleteById).mockImplementation(async ({ id }) => {
|
||||
const team = database.teams.get(id);
|
||||
if (team) {
|
||||
database.teams.delete(id);
|
||||
return team;
|
||||
}
|
||||
throw new Error(`Team with id ${id} not found`);
|
||||
});
|
||||
const mockTeamRepo = {
|
||||
deleteById: vi.fn().mockImplementation(async ({ id }) => {
|
||||
const team = database.teams.get(id);
|
||||
if (team) {
|
||||
database.teams.delete(id);
|
||||
return team;
|
||||
}
|
||||
throw new Error(`Team with id ${id} not found`);
|
||||
}),
|
||||
};
|
||||
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);
|
||||
|
||||
vi.mocked(deleteDomain).mockImplementation(async (slug) => {
|
||||
database.domains.delete(slug);
|
||||
|
||||
@@ -47,14 +47,17 @@ describe("TeamService", () => {
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
vi.mocked(TeamRepository.deleteById).mockResolvedValue(mockDeletedTeam);
|
||||
const mockTeamRepo = {
|
||||
deleteById: vi.fn().mockResolvedValue(mockDeletedTeam),
|
||||
};
|
||||
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);
|
||||
|
||||
const result = await TeamService.delete({ id: 1 });
|
||||
|
||||
expect(TeamBilling.findAndInit).toHaveBeenCalledWith(1);
|
||||
expect(mockTeamBilling.cancel).toHaveBeenCalled();
|
||||
expect(WorkflowService.deleteWorkflowRemindersOfRemovedTeam).toHaveBeenCalledWith(1);
|
||||
expect(TeamRepository.deleteById).toHaveBeenCalledWith({ id: 1 });
|
||||
expect(mockTeamRepo.deleteById).toHaveBeenCalledWith({ id: 1 });
|
||||
expect(deleteDomain).toHaveBeenCalledWith("deleted-team");
|
||||
expect(result).toEqual(mockDeletedTeam);
|
||||
});
|
||||
|
||||
@@ -144,7 +144,8 @@ export class InsightsBookingService {
|
||||
options: Extract<InsightsBookingServiceOptions, { scope: "org" }>
|
||||
): Promise<Prisma.BookingTimeStatusDenormalizedWhereInput> {
|
||||
// Get all teams from the organization
|
||||
const teamsFromOrg = await TeamRepository.findAllByParentId({
|
||||
const teamRepo = new TeamRepository(this.prisma);
|
||||
const teamsFromOrg = await teamRepo.findAllByParentId({
|
||||
parentId: options.orgId,
|
||||
select: { id: true },
|
||||
});
|
||||
@@ -183,7 +184,8 @@ export class InsightsBookingService {
|
||||
private async buildTeamAuthorizationCondition(
|
||||
options: Extract<InsightsBookingServiceOptions, { scope: "team" }>
|
||||
): Promise<Prisma.BookingTimeStatusDenormalizedWhereInput> {
|
||||
const childTeamOfOrg = await TeamRepository.findByIdAndParentId({
|
||||
const teamRepo = new TeamRepository(this.prisma);
|
||||
const childTeamOfOrg = await teamRepo.findByIdAndParentId({
|
||||
id: options.teamId,
|
||||
parentId: options.orgId,
|
||||
select: { id: true },
|
||||
|
||||
@@ -126,7 +126,8 @@ export class InsightsRoutingService {
|
||||
options: Extract<InsightsRoutingServiceOptions, { scope: "org" }>
|
||||
): Promise<Prisma.RoutingFormResponseDenormalizedWhereInput> {
|
||||
// Get all teams from the organization
|
||||
const teamsFromOrg = await TeamRepository.findAllByParentId({
|
||||
const teamRepo = new TeamRepository(this.prisma);
|
||||
const teamsFromOrg = await teamRepo.findAllByParentId({
|
||||
parentId: options.orgId,
|
||||
select: { id: true },
|
||||
});
|
||||
@@ -141,7 +142,8 @@ export class InsightsRoutingService {
|
||||
private async buildTeamAuthorizationCondition(
|
||||
options: Extract<InsightsRoutingServiceOptions, { scope: "team" }>
|
||||
): Promise<Prisma.RoutingFormResponseDenormalizedWhereInput> {
|
||||
const childTeamOfOrg = await TeamRepository.findByIdAndParentId({
|
||||
const teamRepo = new TeamRepository(this.prisma);
|
||||
const childTeamOfOrg = await teamRepo.findByIdAndParentId({
|
||||
id: options.teamId,
|
||||
parentId: options.orgId,
|
||||
select: { id: true },
|
||||
|
||||
@@ -34,7 +34,8 @@ export class TeamService {
|
||||
}
|
||||
|
||||
// Step 3: Delete the team from the database. This is the core "commit" point.
|
||||
const deletedTeam = await TeamRepository.deleteById({ id });
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
const deletedTeam = await teamRepo.deleteById({ id });
|
||||
|
||||
// Step 4: Clean up any final, non-critical external state.
|
||||
if (deletedTeam && deletedTeam.isOrganization && deletedTeam.slug) {
|
||||
|
||||
@@ -292,7 +292,8 @@ export class AvailableSlotsService {
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
|
||||
const team = await TeamRepository.findFirstBySlugAndParentSlug({
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
const team = await teamRepo.findFirstBySlugAndParentSlug({
|
||||
slug,
|
||||
parentSlug: isValidOrgDomain && currentOrgDomain ? currentOrgDomain : null,
|
||||
select: { id: true },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../types";
|
||||
import type { TGetListSchema } from "./list.schema";
|
||||
@@ -11,7 +12,8 @@ type ListOptions = {
|
||||
};
|
||||
|
||||
export const listHandler = async ({ ctx, input }: ListOptions) => {
|
||||
return TeamRepository.findTeamsByUserId({
|
||||
const teamRepo = new TeamRepository(prisma);
|
||||
return teamRepo.findTeamsByUserId({
|
||||
userId: ctx.user.id,
|
||||
includeOrgs: input?.includeOrgs,
|
||||
});
|
||||
|
||||
@@ -2519,7 +2519,7 @@ __metadata:
|
||||
"@axiomhq/winston": ^1.2.0
|
||||
"@calcom/platform-constants": "*"
|
||||
"@calcom/platform-enums": "*"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.252"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.253"
|
||||
"@calcom/platform-types": "*"
|
||||
"@calcom/platform-utils": "*"
|
||||
"@calcom/prisma": "*"
|
||||
@@ -3565,13 +3565,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.252":
|
||||
version: 0.0.252
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.252"
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.253":
|
||||
version: 0.0.253
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.253"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: 70a3a5013e59c4df218464ad63c074e958f53388f7552085048f34083bf2dfac987cc953d21a4ab447d61d08c5e2be74c7557262b8f960885a284f046606af31
|
||||
checksum: 797435f9a72750dc7f8bc6d3b52b1b1fa93701451907f4ae29028403619365649bdc7c5632ac1db326bd6ed1b90e27544d14ae108e042fe18eaeac4ca11e8606
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user