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:
devin-ai-integration[bot]
2025-07-10 14:03:50 +00:00
committed by GitHub
co-authored by morgan@cal.com Morgan
parent 7d02d1bb23
commit efd15c5a3e
15 changed files with 85 additions and 55 deletions
+1 -1
View File
@@ -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: {
+19 -16
View File
@@ -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 },
+2 -1
View File
@@ -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,
});
+5 -5
View File
@@ -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