refactor: convert getShouldServeCache to CacheService with dependency injection (#22814)

* refactor: convert getShouldServeCache to CacheService with dependency injection

- Create CacheService class following AvailableSlotsService DI pattern
- Add FeaturesRepository and CacheService to DI tokens and modules
- Create cache container with proper dependency injection setup
- Update handleNewBooking.ts and slots/util.ts to use new service
- Maintain backward compatibility with error-throwing wrapper function
- Follow established service patterns for clean architecture

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* feat: inject CacheService into AvailableSlotsService via dependency injection

- Add cacheService to IAvailableSlotsService interface
- Update available-slots container to load cache modules
- Update available-slots module to inject CacheService dependency
- Replace direct getShouldServeCache call with injected service method
- Add CacheService import to util.ts for proper typing

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* chore: DI api v2 cache service

* refactor: convert FeaturesRepository to use factory pattern in DI

- Change from constructor injection to factory pattern to avoid PRISMA_CLIENT binding issues in tests
- FeaturesRepository now uses default prisma instance instead of DI injection
- Resolves test failures while maintaining DI container compatibility
- Tests reduced from 123+ failures to only 5 unrelated failures

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* revert: use direct FeaturesRepository instantiation in most usage points

- Revert getFeaturesRepository() calls back to new FeaturesRepository()
- Tests require direct instantiation for mocking compatibility
- Keep DI container for specific use cases that need dependency injection
- Resolves test failures while maintaining both DI and direct usage patterns

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: resolve FeaturesRepository DI container issues

- Update cache module to use factory pattern with proper ICacheService interface
- Remove featuresModule loading from cache and available-slots containers
- Use direct FeaturesRepository instantiation via getFeaturesRepository()
- Resolves 'No binding found for key: Symbol(FeaturesRepository)' errors
- Reduces test failures from 125 to 7 (remaining failures appear unrelated)

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: update all FeaturesRepository instantiations to include prisma parameter

- Add prisma parameter to all new FeaturesRepository() calls across the codebase
- Update API v2 services to match main repo interfaces
- Fix PrismaFeaturesRepository to implement IFeaturesRepository directly
- Update CacheService in API v2 to expose required dependencies and getShouldServeCache
- Implement CheckBookingLimitsService in API v2 with proper interface
- Resolves type assignment errors between API v2 and main repo implementations

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: add prisma parameter to remaining FeaturesRepository instantiations in apps/web/lib

- Update getServerSideProps files to pass prisma parameter to FeaturesRepository
- Ensures all FeaturesRepository instantiations follow the new constructor pattern
- Completes the refactoring to use direct instantiation with prisma parameter

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* refactor clean and fix devin issues

* chore: bump platform libs

* chore: bump platform libs

* chore: bump platform libs

* chore: bump platform libs

* fix: missing di

* fix workflow test

* fix workflow test

* fix integration test

---------

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-31 20:39:27 +03:00
committed by GitHub
co-authored by morgan@cal.com Morgan
parent ad4af64036
commit 1d1a242a72
51 changed files with 179 additions and 74 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.271",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.276",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -1,5 +1,6 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
@@ -7,6 +8,7 @@ import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@@ -22,7 +24,9 @@ import { Module } from "@nestjs/common";
PrismaEventTypeRepository,
PrismaRoutingFormResponseRepository,
PrismaTeamRepository,
PrismaFeaturesRepository,
CheckBookingLimitsService,
CacheService,
AvailableSlotsService,
],
exports: [AvailableSlotsService],
@@ -0,0 +1,12 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { PrismaFeaturesRepository as PrismaFeaturesRepositoryLib } from "@calcom/platform-libraries/repositories";
import { PrismaClient } from "@calcom/prisma";
@Injectable()
export class PrismaFeaturesRepository extends PrismaFeaturesRepositoryLib {
constructor(private readonly dbWrite: PrismaWriteService) {
super(dbWrite.prisma as unknown as PrismaClient);
}
}
@@ -1,11 +1,13 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected-slot.repository";
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { Injectable } from "@nestjs/common";
@@ -21,7 +23,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
bookingRepository: PrismaBookingRepository,
selectedSlotRepository: PrismaSelectedSlotRepository,
eventTypeRepository: PrismaEventTypeRepository,
userRepository: PrismaUserRepository
userRepository: PrismaUserRepository,
featuresRepository: PrismaFeaturesRepository
) {
super({
oooRepo: oooRepoDependency,
@@ -32,7 +35,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
selectedSlotRepo: selectedSlotRepository,
eventTypeRepo: eventTypeRepository,
userRepo: userRepository,
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository),
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository) as any,
cacheService: new CacheService(featuresRepository),
});
}
}
@@ -0,0 +1,11 @@
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { Injectable } from "@nestjs/common";
import { CacheService as BaseCacheService } from "@calcom/platform-libraries";
@Injectable()
export class CacheService extends BaseCacheService {
constructor(featuresRepository: PrismaFeaturesRepository) {
super({ featuresRepository });
}
}
@@ -7,6 +7,7 @@ import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
@@ -17,7 +18,7 @@ import type { PageProps as LegacyPageProps } from "~/team/type-view";
import CachedTeamBooker, { generateMetadata as generateCachedMetadata } from "./pageWithCachedData";
async function isCachedTeamBookingEnabled(searchParams: SearchParams): Promise<boolean> {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const isGloballyEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
"team-booking-page-cache"
);
@@ -102,7 +102,7 @@ export async function getEnrichedEventType({
}
export async function shouldUseApiV2ForTeamSlots(teamId: number): Promise<boolean> {
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(teamId, "use-api-v2-for-team-slots");
const useApiV2 = teamHasApiV2Route && Boolean(process.env.NEXT_PUBLIC_API_V2_URL);
@@ -4,11 +4,12 @@ import { notFound } from "next/navigation";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import Shell from "@calcom/features/shell/Shell";
import { prisma } from "@calcom/prisma";
import UpgradeTipWrapper from "./UpgradeTipWrapper";
export default async function InsightsLayout({ children }: { children: React.ReactNode }) {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
if (!insightsEnabled) {
@@ -9,6 +9,7 @@ import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
import { Resource, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -17,7 +18,7 @@ import SettingsLayoutAppDirClient from "./SettingsLayoutAppDirClient";
const getTeamFeatures = unstable_cache(
async (teamId: number) => {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
return await featuresRepository.getTeamFeatures(teamId);
},
["team-features"],
@@ -11,6 +11,7 @@ import { Resource, CrudAction } from "@calcom/features/pbac/domain/types/permiss
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -29,7 +30,7 @@ const getCachedTeamRoles = unstable_cache(
const getCachedTeamFeature = unstable_cache(
async (teamId: number, feature: keyof AppFlags) => {
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const res = await featureRepo.checkIfTeamHasFeature(teamId, feature);
return res;
},
@@ -12,6 +12,7 @@ import { PermissionCheckService } from "@calcom/features/pbac/services/permissio
import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -32,7 +33,7 @@ const getCachedTeamRoles = (teamId: number) =>
const getCachedTeamFeature = (teamId: number, feature: keyof AppFlags) =>
unstable_cache(
async () => {
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const res = await featureRepo.checkIfTeamHasFeature(teamId, feature);
return res;
},
+2 -1
View File
@@ -10,6 +10,7 @@ import getIP from "@calcom/lib/getIP";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
import { prisma } from "@calcom/prisma";
import { signupSchema } from "@calcom/prisma/zod-utils";
async function ensureSignupIsEnabled(body: Record<string, string>) {
@@ -22,7 +23,7 @@ async function ensureSignupIsEnabled(body: Record<string, string>) {
// Still allow signups if there is a team invite
if (token) return;
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const signupDisabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("disable-signup");
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" || signupDisabled) {
@@ -120,7 +120,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
// Check if team has API v2 feature flag enabled (same logic as team pages)
let useApiV2 = false;
if (isTeamEvent && hashedLink.eventType.team?.id) {
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(
hashedLink.eventType.team.id,
"use-api-v2-for-team-slots"
@@ -1,9 +1,10 @@
import type { GetServerSidePropsResult } from "next";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { prisma } from "@calcom/prisma";
export const getServerSideProps = async (): Promise<GetServerSidePropsResult<{ isOrg: boolean }>> => {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
// Check if organizations are enabled
if (!organizations) {
+1 -1
View File
@@ -24,7 +24,7 @@ const querySchema = z.object({
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const emailVerificationEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
"email-verification"
);
@@ -127,7 +127,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const organizationSettings = getOrganizationSEOSettings(team);
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(team.id, "use-api-v2-for-team-slots");
const useApiV2 = teamHasApiV2Route && hasApiV2RouteInEnv();
@@ -71,7 +71,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
// Provided by Rewrite from next.config.js
const isOrgProfile = context.query?.isOrgProfile === "1";
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const organizationsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
log.debug("getServerSideProps", {
@@ -247,7 +247,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const videoReference = getCalVideoReference(bookingObj.references);
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const displayLogInOverlay = profile?.organizationId
? await featureRepo.checkIfTeamHasFeature(profile.organizationId, "cal-video-log-in-overlay")
: false;
+2 -1
View File
@@ -7,6 +7,7 @@ import { getErrorFromUnknown } from "@calcom/lib/errors";
import isSmsCalEmail from "@calcom/lib/isSmsCalEmail";
import { serverConfig } from "@calcom/lib/serverConfig";
import { setTestEmail } from "@calcom/lib/testEmails";
import { prisma } from "@calcom/prisma";
import { sanitizeDisplayName } from "../lib/sanitizeDisplayName";
@@ -29,7 +30,7 @@ export default class BaseEmail {
return {};
}
public async sendEmail() {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const emailsDisabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("emails");
/** If email kill switch exists and is active, we prevent emails being sent. */
if (emailsDisabled) {
+2 -2
View File
@@ -34,7 +34,7 @@ export const sendEmailVerification = async ({
}: VerifyEmailType) => {
const token = randomBytes(32).toString("hex");
const translation = await getTranslation(language ?? "en", "common");
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
if (!emailVerification) {
@@ -127,7 +127,7 @@ interface ChangeOfEmail {
export const sendChangeOfEmailVerification = async ({ user, language }: ChangeOfEmail) => {
const token = randomBytes(32).toString("hex");
const translation = await getTranslation(language ?? "en", "common");
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
if (!emailVerification) {
@@ -27,7 +27,6 @@ import getICalUID from "@calcom/emails/lib/getICalUID";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import { getShouldServeCache } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
import {
allowDisablingAttendeeConfirmationEmails,
@@ -52,6 +51,7 @@ import {
getFirstDelegationConferencingCredentialAppLocation,
} from "@calcom/lib/delegationCredential/server";
import { getCheckBookingAndDurationLimitsService } from "@calcom/lib/di/containers/booking-limits";
import { getCacheService } from "@calcom/lib/di/containers/cache";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { getEventName, updateHostInEventName } from "@calcom/lib/event";
@@ -539,7 +539,8 @@ async function handler(
}
}
const shouldServeCache = await getShouldServeCache(_shouldServeCache, eventType.team?.id);
const cacheService = getCacheService();
const shouldServeCache = await cacheService.getShouldServeCache(_shouldServeCache, eventType.team?.id);
const isTeamEventType =
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
@@ -3,6 +3,7 @@ import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { getCredentialForCalendarCache } from "@calcom/lib/delegationCredential/server";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import type { Calendar } from "@calcom/types/Calendar";
import { CalendarCacheRepository } from "./calendar-cache.repository";
@@ -20,7 +21,7 @@ export class CalendarCache {
return await CalendarCache.init(calendarForCalendarCache);
}
static async init(calendar: Calendar | null): Promise<ICalendarCacheRepository> {
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const isCalendarCacheEnabledGlobally = await featureRepo.checkIfFeatureIsEnabledGlobally(
"calendar-cache"
);
@@ -1,8 +1,15 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import type { IFeaturesRepository } from "@calcom/features/flags/features.repository.interface";
export async function getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) {
if (typeof shouldServeCache === "boolean") return shouldServeCache;
if (!teamId) return undefined;
const featureRepo = new FeaturesRepository();
return await featureRepo.checkIfTeamHasFeature(teamId, "calendar-cache-serve");
export interface ICacheService {
featuresRepository: IFeaturesRepository;
}
export class CacheService {
constructor(private readonly dependencies: ICacheService) {}
async getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) {
if (typeof shouldServeCache === "boolean") return shouldServeCache;
if (!teamId) return undefined;
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId, "calendar-cache-serve");
}
}
@@ -6,7 +6,7 @@ import { MembershipRole } from "@calcom/prisma/client";
export const getServerSideProps = async ({ req }: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const organizationsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
// Check if organizations are enabled
if (!organizationsEnabled) {
@@ -277,7 +277,7 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
const isSendgridEnabled = !!(process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL);
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const isWorkflowSmtpEmailsEnabled = teamId
? await featureRepo.checkIfTeamHasFeature(teamId, "workflow-smtp-emails")
@@ -1103,7 +1103,6 @@ describe("Workflow SMTP Emails Feature Flag", () => {
});
test("should use SMTP when team has workflow-smtp-emails feature", async () => {
const featuresRepository = new FeaturesRepository();
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(true);
await scheduleEmailReminder({
@@ -1115,7 +1114,6 @@ describe("Workflow SMTP Emails Feature Flag", () => {
});
test("should use SMTP when user has workflow-smtp-emails feature", async () => {
const featuresRepository = new FeaturesRepository();
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(true);
await scheduleEmailReminder({
@@ -1127,7 +1125,6 @@ describe("Workflow SMTP Emails Feature Flag", () => {
});
test("should use SendGrid when workflow-smtp-emails feature is not enabled for team", async () => {
const featuresRepository = new FeaturesRepository();
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(false);
await scheduleEmailReminder({
@@ -1140,7 +1137,6 @@ describe("Workflow SMTP Emails Feature Flag", () => {
});
test("should use SendGrid when workflow-smtp-emails feature is not enabled for user", async () => {
const featuresRepository = new FeaturesRepository();
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(false);
await scheduleEmailReminder({
@@ -1153,9 +1149,9 @@ describe("Workflow SMTP Emails Feature Flag", () => {
});
test("should use SMTP when SendGrid is not configured", async () => {
const featuresRepository = new FeaturesRepository();
vi.spyOn(featuresRepository, "checkIfTeamHasFeature").mockResolvedValue(false);
vi.spyOn(featuresRepository, "checkIfUserHasFeature").mockResolvedValue(false);
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(false);
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(false);
delete process.env.SENDGRID_API_KEY;
delete process.env.SENDGRID_EMAIL;
@@ -1,11 +1,11 @@
import { PrismaClient } from "@prisma/client";
import { describe, expect, beforeAll, afterAll, beforeEach, it } from "vitest";
import prisma from "@calcom/prisma";
import type { AppFlags } from "./config";
import { FeaturesRepository } from "./features.repository";
const prisma = new PrismaClient();
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
// Access private clearCache method through type assertion
const clearCache = () => {
@@ -1,7 +1,7 @@
import { Prisma } from "@prisma/client";
import { captureException } from "@sentry/nextjs";
import db from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
import type { AppFlags, TeamFeatures } from "./config";
import type { IFeaturesRepository } from "./features.repository.interface";
@@ -18,6 +18,8 @@ interface CacheOptions {
export class FeaturesRepository implements IFeaturesRepository {
private static featuresCache: { data: any[]; expiry: number } | null = null;
constructor(private prismaClient: PrismaClient) {}
private clearCache() {
FeaturesRepository.featuresCache = null;
}
@@ -32,7 +34,7 @@ export class FeaturesRepository implements IFeaturesRepository {
return FeaturesRepository.featuresCache.data;
}
const features = await db.feature.findMany({
const features = await this.prismaClient.feature.findMany({
orderBy: { slug: "asc" },
cacheStrategy: { swr: 300, ttl: 300 },
});
@@ -64,7 +66,7 @@ export class FeaturesRepository implements IFeaturesRepository {
* @returns Promise<{ [slug: string]: boolean } | null>
*/
public async getTeamFeatures(teamId: number) {
const result = await db.teamFeatures.findMany({
const result = await this.prismaClient.teamFeatures.findMany({
where: {
teamId,
},
@@ -122,7 +124,7 @@ export class FeaturesRepository implements IFeaturesRepository {
* FIXME refactor when upgrading prismock
* https://github.com/morintd/prismock/issues/592
*/
const userHasFeature = await db.userFeatures.findFirst({
const userHasFeature = await this.prismaClient.userFeatures.findFirst({
where: {
userId,
featureId: slug,
@@ -180,7 +182,7 @@ export class FeaturesRepository implements IFeaturesRepository {
LIMIT 1;
`;
const result = await db.$queryRaw<unknown[]>(query);
const result = await this.prismaClient.$queryRaw<unknown[]>(query);
return result.length > 0;
} catch (err) {
captureException(err);
@@ -199,7 +201,7 @@ export class FeaturesRepository implements IFeaturesRepository {
async checkIfTeamHasFeature(teamId: number, featureId: keyof AppFlags): Promise<boolean> {
try {
// Early return if team has feature directly assigned
const teamHasFeature = await db.teamFeatures.findUnique({
const teamHasFeature = await this.prismaClient.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId,
@@ -238,7 +240,7 @@ export class FeaturesRepository implements IFeaturesRepository {
LIMIT 1;
`;
const result = await db.$queryRaw<unknown[]>(query);
const result = await this.prismaClient.$queryRaw<unknown[]>(query);
return result.length > 0;
} catch (err) {
captureException(err);
@@ -1,6 +1,7 @@
import { startSpan } from "@sentry/nextjs";
import { FeaturesRepository } from "../features.repository";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { prisma } from "@calcom/prisma";
/**
* Use Cases represent individual operations, like "Create Feature" or "Sign In" or "Toggle Feature".
@@ -11,7 +12,7 @@ import { FeaturesRepository } from "../features.repository";
*/
export function checkIfUserHasFeatureUseCase(userId: number, slug: string): Promise<boolean> {
return startSpan({ name: "checkIfUserHasFeature UseCase", op: "function" }, async () => {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
return await featuresRepository.checkIfUserHasFeature(userId, slug);
});
@@ -1,12 +1,12 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import prisma from "@calcom/prisma";
import publicProcedure from "@calcom/trpc/server/procedures/publicProcedure";
import { FeaturesRepository } from "../../features.repository";
/**
* TRPC procedure that returns a map of all feature flags and their enabled status.
* Uses the FeaturesRepository to handle caching and database access.
*/
export const map = publicProcedure.query(async () => {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
return featuresRepository.getFeatureFlagMap();
});
+4 -3
View File
@@ -1,15 +1,16 @@
import { z } from "zod";
import type { AppFlags } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { prisma } from "@calcom/prisma";
import publicProcedure from "@calcom/trpc/server/procedures/publicProcedure";
import { router } from "@calcom/trpc/server/trpc";
import { FeaturesRepository } from "../features.repository";
import { map } from "./procedures/map";
export const featureFlagRouter = router({
list: publicProcedure.query(async () => {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
return featuresRepository.getAllFeatures();
}),
checkTeamFeature: publicProcedure
@@ -20,7 +21,7 @@ export const featureFlagRouter = router({
})
)
.query(async ({ input }) => {
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
return featuresRepository.checkIfTeamHasFeature(input.teamId, input.feature as keyof AppFlags);
}),
map,
@@ -1,4 +1,5 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { prisma } from "@calcom/prisma";
import type { MembershipRole } from "@calcom/prisma/enums";
import { PermissionMapper } from "../domain/mappers/PermissionMapper";
@@ -53,7 +54,7 @@ export const getResourcePermissions = async ({
userRole,
fallbackRoles = {},
}: ResourcePermissionsOptions): Promise<ResourcePermissions> => {
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const permissionService = new PermissionCheckService();
const pbacEnabled = await featureRepo.checkIfTeamHasFeature(teamId, "pbac");
@@ -1,6 +1,7 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import logger from "@calcom/lib/logger";
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
import prisma from "@calcom/prisma";
import type { MembershipRole } from "@calcom/prisma/enums";
import { PermissionMapper } from "../domain/mappers/PermissionMapper";
@@ -24,7 +25,7 @@ export class PermissionCheckService {
constructor(
private readonly repository: IPermissionRepository = new PermissionRepository(),
featuresRepository: FeaturesRepository = new FeaturesRepository(),
featuresRepository: FeaturesRepository = new FeaturesRepository(prisma),
permissionService: PermissionService = new PermissionService()
) {
this.featuresRepository = featuresRepository;
@@ -150,8 +150,7 @@ export class RoleManagementFactory {
private permissionCheckService: PermissionCheckService;
private constructor() {
this.featuresRepository = new FeaturesRepository();
this.roleService = new RoleService();
(this.featuresRepository = new FeaturesRepository(prisma)), (this.roleService = new RoleService());
this.permissionCheckService = new PermissionCheckService();
}
+1 -1
View File
@@ -1153,7 +1153,7 @@ export default class EventManager {
private async createAllCRMEvents(event: CalendarEvent) {
const createdEvents = [];
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const isTaskerEnabledForSalesforceCrm = event.team?.id
? await featureRepo.checkIfTeamHasFeature(event.team.id, "salesforce-crm-tasker")
: false;
@@ -6,8 +6,10 @@ import type { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/s
import { availableSlotsModule } from "../modules/available-slots";
import { bookingRepositoryModule } from "../modules/booking";
import { cacheModule } from "../modules/cache";
import { checkBookingLimitsModule } from "../modules/check-booking-limits";
import { eventTypeRepositoryModule } from "../modules/eventType";
import { featuresRepositoryModule } from "../modules/features";
import { oooRepositoryModule } from "../modules/ooo";
import { routingFormResponseRepositoryModule } from "../modules/routingFormResponse";
import { scheduleRepositoryModule } from "../modules/schedule";
@@ -25,6 +27,8 @@ container.load(DI_TOKENS.USER_REPOSITORY_MODULE, userRepositoryModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
container.load(DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY_MODULE, routingFormResponseRepositoryModule);
container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule);
container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule);
container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule);
container.load(DI_TOKENS.AVAILABLE_SLOTS_SERVICE_MODULE, availableSlotsModule);
+17
View File
@@ -0,0 +1,17 @@
import { createContainer } from "@evyweb/ioctopus";
import { DI_TOKENS } from "@calcom/lib/di/tokens";
import { prismaModule } from "@calcom/prisma/prisma.module";
import type { CacheService } from "../../../features/calendar-cache/lib/getShouldServeCache";
import { cacheModule } from "../modules/cache";
import { featuresRepositoryModule } from "../modules/features";
const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule);
container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule);
export function getCacheService() {
return container.get<CacheService>(DI_TOKENS.CACHE_SERVICE);
}
@@ -15,5 +15,6 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
eventTypeRepo: DI_TOKENS.EVENT_TYPE_REPOSITORY,
routingFormResponseRepo: DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY,
cacheService: DI_TOKENS.CACHE_SERVICE,
checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE,
} satisfies Record<keyof IAvailableSlotsService, symbol>);
+11
View File
@@ -0,0 +1,11 @@
import { createModule } from "@evyweb/ioctopus";
import type { ICacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
import { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
import { DI_TOKENS } from "../tokens";
export const cacheModule = createModule();
cacheModule.bind(DI_TOKENS.CACHE_SERVICE).toClass(CacheService, {
featuresRepository: DI_TOKENS.FEATURES_REPOSITORY,
} satisfies Record<keyof ICacheService, symbol>);
+9
View File
@@ -0,0 +1,9 @@
import { createModule } from "@evyweb/ioctopus";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { DI_TOKENS } from "@calcom/lib/di/tokens";
export const featuresRepositoryModule = createModule();
featuresRepositoryModule
.bind(DI_TOKENS.FEATURES_REPOSITORY)
.toClass(FeaturesRepository, [DI_TOKENS.PRISMA_CLIENT]);
+4
View File
@@ -22,6 +22,10 @@ export const DI_TOKENS = {
AVAILABLE_SLOTS_SERVICE_MODULE: Symbol("AvailableSlotsModule"),
INSIGHTS_ROUTING_SERVICE: Symbol("InsightsRoutingService"),
INSIGHTS_ROUTING_SERVICE_MODULE: Symbol("InsightsRoutingServiceModule"),
FEATURES_REPOSITORY: Symbol("FeaturesRepository"),
FEATURES_REPOSITORY_MODULE: Symbol("FeaturesRepositoryModule"),
CACHE_SERVICE: Symbol("CacheService"),
CACHE_SERVICE_MODULE: Symbol("CacheServiceModule"),
CHECK_BOOKING_LIMITS_SERVICE: Symbol("CheckBookingLimitsService"),
CHECK_BOOKING_LIMITS_SERVICE_MODULE: Symbol("CheckBookingLimitsServiceModule"),
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE: Symbol("CheckBookingAndDurationLimitsService"),
+2 -1
View File
@@ -1,4 +1,5 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import prisma from "@calcom/prisma";
/**
* Checks if the restriction schedule feature is enabled for a team
@@ -10,6 +11,6 @@ export async function isRestrictionScheduleEnabled(teamId?: number): Promise<boo
return false; // Personal events don't have restriction schedules
}
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
return await featureRepo.checkIfTeamHasFeature(teamId, "restriction-schedule");
}
+2
View File
@@ -119,3 +119,5 @@ export type { TFindTeamMembersMatchingAttributeLogicInputSchema } from "@calcom/
export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
export { verifyPhoneNumber, sendVerificationCode };
export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
@@ -6,3 +6,4 @@ export { RoutingFormResponseRepository as PrismaRoutingFormResponseRepository }
export { PrismaSelectedSlotRepository } from "@calcom/lib/server/repository/PrismaSelectedSlotRepository";
export { TeamRepository as PrismaTeamRepository } from "@calcom/lib/server/repository/team";
export { UserRepository as PrismaUserRepository } from "@calcom/lib/server/repository/user";
export { FeaturesRepository as PrismaFeaturesRepository } from "@calcom/features/flags/features.repository";
@@ -1,5 +1,6 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import { TRPCError } from "@trpc/server";
@@ -22,7 +23,7 @@ const checkDelegationCredentialFeature = async ({
}) => {
const user = ctx.user;
const t = await getTranslation(user.locale ?? "en", "common");
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
if (!user.organizationId) {
throw new TRPCError({
@@ -40,7 +40,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
const billingService = new StripeBillingService();
const userMetadata = handleUserMetadata({ ctx, input });
const locale = input.locale || user.locale;
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
const { travelSchedules, ...rest } = input;
@@ -1,5 +1,6 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import prisma from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
@@ -26,7 +27,7 @@ export const listHandler = async ({ ctx }: ListHandlerInput) => {
return currentOrg;
}
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const hasDelegationCredential = await featureRepo.checkIfTeamHasFeature(
organizationId,
"delegation-credential"
@@ -39,7 +39,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
const expand = input.expand;
const filters = input.filters || [];
const featuresRepository = new FeaturesRepository();
const featuresRepository = new FeaturesRepository(prisma);
const pbacFeatureEnabled = await featuresRepository.checkIfTeamHasFeature(organizationId, "pbac");
const allAttributeOptions = await prisma.attributeOption.findMany({
@@ -5,6 +5,7 @@ import { Resource, CrudAction, CustomAction } from "@calcom/features/pbac/domain
import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { RoleService } from "@calcom/features/pbac/services/role.service";
import prisma from "@calcom/prisma";
import { RoleType, MembershipRole } from "@calcom/prisma/enums";
import authedProcedure from "../../../procedures/authedProcedure";
@@ -193,7 +194,7 @@ export const permissionsRouter = router({
throw new Error("Unauthorized");
}
const featureRepo = new FeaturesRepository();
const featureRepo = new FeaturesRepository(prisma);
const teamHasPBACFeature = await featureRepo.checkIfTeamHasFeature(input.teamId, "pbac");
if (!teamHasPBACFeature) {
@@ -7,7 +7,7 @@ import dayjs from "@calcom/dayjs";
import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import { getShouldServeCache } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
import { findQualifiedHostsWithDelegationCredentials } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials";
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
@@ -99,6 +99,7 @@ export interface IAvailableSlotsService {
bookingRepo: BookingRepository;
eventTypeRepo: EventTypeRepository;
routingFormResponseRepo: RoutingFormResponseRepository;
cacheService: CacheService;
checkBookingLimitsService: CheckBookingLimitsService;
}
@@ -909,8 +910,10 @@ export class AvailableSlotsService {
throw new TRPCError({ code: "NOT_FOUND" });
}
// TODO: DI getShouldServeCache
const shouldServeCache = await getShouldServeCache(_shouldServeCache, eventType.team?.id);
const shouldServeCache = await this.dependencies.cacheService.getShouldServeCache(
_shouldServeCache,
eventType.team?.id
);
if (isEventTypeLoggingEnabled({ eventTypeId: eventType.id })) {
logger.settings.minLevel = 2;
}
+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.271"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.276"
"@calcom/platform-types": "*"
"@calcom/platform-utils": "*"
"@calcom/prisma": "*"
@@ -3575,13 +3575,13 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.271":
version: 0.0.271
resolution: "@calcom/platform-libraries@npm:0.0.271"
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.276":
version: 0.0.276
resolution: "@calcom/platform-libraries@npm:0.0.276"
dependencies:
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 5b55a05c5cd1d471fe36c90b8b2f16e70f6155065b3b13e12c06bde73f3ceffde74de3921c3a9fe05d04eed7fb587836ac79a46a521d35fe83812d66b13d8733
checksum: 4effc186c0301ec35496ebffbe32280f26261667b60057d18207b30636b038cdcc3783b95390eecc8460f3203d0cfe5d732e3b3f89031ff064b1a556321779a8
languageName: node
linkType: hard