Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BookingAuditTaskConsumer } from "@calcom/features/booking-audit/lib/ser
import { BOOKING_AUDIT_DI_TOKENS } from "@calcom/features/booking-audit/di/tokens";
import { moduleLoader as bookingAuditRepositoryModuleLoader } from "@calcom/features/booking-audit/di/BookingAuditRepository.module";
import { moduleLoader as auditActorRepositoryModuleLoader } from "@calcom/features/booking-audit/di/AuditActorRepository.module";
import { moduleLoader as featuresRepositoryModuleLoader } from "@calcom/features/di/modules/Features";
import { moduleLoader as featuresRepositoryModuleLoader } from "@calcom/features/di/modules/FeaturesRepository";
import { moduleLoader as userRepositoryModuleLoader } from "@calcom/features/di/modules/User";

import { createModule, bindModuleToClassOnToken } from "../../di/di";
Expand All @@ -28,4 +28,3 @@ export const moduleLoader = {
token,
loadModule
};

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RegularBookingService } from "@calcom/features/bookings/lib/service/Reg
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as bookingRepositoryModuleLoader } from "@calcom/features/di/modules/Booking";
import { moduleLoader as checkBookingAndDurationLimitsModuleLoader } from "@calcom/features/di/modules/CheckBookingAndDurationLimits";
import { moduleLoader as featuresRepositoryModuleLoader } from "@calcom/features/di/modules/Features";
import { moduleLoader as featuresRepositoryModuleLoader } from "@calcom/features/di/modules/FeaturesRepository";
import { moduleLoader as luckyUserServiceModuleLoader } from "@calcom/features/di/modules/LuckyUser";
import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma";
import { moduleLoader as userRepositoryModuleLoader } from "@calcom/features/di/modules/User";
Expand Down
2 changes: 1 addition & 1 deletion packages/features/di/containers/AvailableSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { bookingRepositoryModule } from "../modules/Booking";
import { busyTimesModule } from "../modules/BusyTimes";
import { checkBookingLimitsModule } from "../modules/CheckBookingLimits";
import { eventTypeRepositoryModule } from "../modules/EventType";
import { featuresRepositoryModule } from "../modules/Features";
import { featuresRepositoryModule } from "../modules/FeaturesRepository";
import { filterHostsModule } from "../modules/FilterHosts";
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
import { holidayRepositoryModule } from "../modules/Holiday";
Expand Down
10 changes: 10 additions & 0 deletions packages/features/di/containers/FeatureOptInService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { IFeatureOptInService } from "@calcom/features/feature-opt-in/services/IFeatureOptInService";

import { createContainer } from "../di";
import { moduleLoader as featureOptInServiceModuleLoader } from "../modules/FeatureOptInService";

export function getFeatureOptInService(): IFeatureOptInService {
const featureOptInServiceContainer = createContainer();
featureOptInServiceModuleLoader.loadModule(featureOptInServiceContainer);
return featureOptInServiceContainer.get<IFeatureOptInService>(featureOptInServiceModuleLoader.token);
}
8 changes: 8 additions & 0 deletions packages/features/di/containers/FeaturesRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContainer } from "../di";
import { type FeaturesRepository, moduleLoader as featuresRepositoryModuleLoader } from "../modules/FeaturesRepository";

export function getFeaturesRepository(): FeaturesRepository {
const featuresRepositoryContainer = createContainer();
featuresRepositoryModuleLoader.loadModule(featuresRepositoryContainer);
return featuresRepositoryContainer.get<FeaturesRepository>(featuresRepositoryModuleLoader.token);
}
24 changes: 24 additions & 0 deletions packages/features/di/modules/FeatureOptInService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FEATURE_OPT_IN_DI_TOKENS } from "@calcom/features/feature-opt-in/di/tokens";
import { FeatureOptInService } from "@calcom/features/feature-opt-in/services/FeatureOptInService";

import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "../di";
import { moduleLoader as featuresRepositoryModuleLoader } from "./FeaturesRepository";

const thisModule = createModule();
const token = FEATURE_OPT_IN_DI_TOKENS.FEATURE_OPT_IN_SERVICE;
const moduleToken = FEATURE_OPT_IN_DI_TOKENS.FEATURE_OPT_IN_SERVICE_MODULE;

const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: FeatureOptInService,
dep: featuresRepositoryModuleLoader,
});

export const moduleLoader: ModuleLoader = {
token,
loadModule,
};

export type { FeatureOptInService };
16 changes: 0 additions & 16 deletions packages/features/di/modules/Features.ts

This file was deleted.

24 changes: 24 additions & 0 deletions packages/features/di/modules/FeaturesRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FLAGS_DI_TOKENS } from "@calcom/features/flags/di/tokens";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";

import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "../di";
import { moduleLoader as prismaModuleLoader } from "./Prisma";

export const featuresRepositoryModule = createModule();
const token = FLAGS_DI_TOKENS.FEATURES_REPOSITORY;
const moduleToken = FLAGS_DI_TOKENS.FEATURES_REPOSITORY_MODULE;

const loadModule = bindModuleToClassOnToken({
module: featuresRepositoryModule,
moduleToken,
token,
classs: FeaturesRepository,
dep: prismaModuleLoader,
});

export const moduleLoader: ModuleLoader = {
token: moduleToken,
loadModule,
};
Comment on lines +19 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the FeaturesRepository.ts file
fd -n "FeaturesRepository.ts"

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 316


🏁 Script executed:

# Once found, read the entire file to understand the context
head -50 packages/features/di/modules/FeaturesRepository.ts

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 842


🏁 Script executed:

# Search for how bindModuleToClassOnToken is used with this module
rg "bindModuleToClassOnToken" -B 2 -A 2 --max-count 10

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 45311


🏁 Script executed:

# Search for FEATURES_REPOSITORY token definition
rg "FEATURES_REPOSITORY" --max-count 20

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 719


🏁 Script executed:

# Search for how moduleLoader.token is used (imports of moduleLoader from FeaturesRepository)
rg "moduleLoader.*token|\.token\s*[=:)]" packages/features/di/modules/ -B 2 -A 2 | head -100

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 68


🏁 Script executed:

# Check if moduleLoader is imported and how it's used
rg "featuresRepositoryModuleLoader|from.*FeaturesRepository" --max-count 20 -B 2 -A 2

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 7075


🏁 Script executed:

# Look at the bindModuleToClassOnToken function definition to understand what token param means
rg -A 20 "export function bindModuleToClassOnToken" packages/features/di/di.ts | head -50

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 1574


🏁 Script executed:

# Check how other modules with the same pattern (token: moduleToken) are used in their containers
rg "container.get.*moduleLoader.token" -B 5 -A 5

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 68


🏁 Script executed:

# Look at a similar module's container to see the pattern
cat packages/features/di/containers/User.ts

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 157


🏁 Script executed:

# Find container files
fd "container" packages/features/di/containers/ -type f

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 256


🏁 Script executed:

# Look at the FeaturesRepository container again more carefully
cat packages/features/di/containers/FeaturesRepository.ts

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 543


🏁 Script executed:

# Search for other container.get calls with moduleLoader tokens to understand the pattern
rg "container.get" packages/features/di/containers/ -B 3 -A 1

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 6714


🏁 Script executed:

# Verify the bindModuleToClassOnToken actually binds to the `token` parameter, not moduleToken
rg -A 50 "export function bindModuleToClassOnToken" packages/features/di/di.ts | grep -A 40 "module.bind"

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 2588


🏁 Script executed:

# Double-check by looking at actual implementation in di.ts
sed -n '1,200p' packages/features/di/di.ts | tail -100

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 3187


Change moduleLoader.token to export the repository token, not the module token.

bindModuleToClassOnToken binds the FeaturesRepository class to FLAGS_DI_TOKENS.FEATURES_REPOSITORY (the token parameter), but moduleLoader.token currently exports moduleToken (FLAGS_DI_TOKENS.FEATURES_REPOSITORY_MODULE). This causes container.get(moduleLoader.token) in the FeaturesRepository container to fail, as it attempts to retrieve using the wrong token.

✅ Suggested fix
 export const moduleLoader: ModuleLoader = {
-  token: moduleToken,
+  token,
   loadModule,
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const moduleLoader: ModuleLoader = {
token: moduleToken,
loadModule,
};
export const moduleLoader: ModuleLoader = {
token,
loadModule,
};
🤖 Prompt for AI Agents
In `@packages/features/di/modules/FeaturesRepository.ts` around lines 19 - 22,
moduleLoader currently exports moduleToken but should export the repository
token; update the ModuleLoader object (moduleLoader.token) to use the
FEATURES_REPOSITORY token (the same token used in
bindModuleToClassOnToken/FLAGS_DI_TOKENS.FEATURES_REPOSITORY) rather than
moduleToken/FEATURES_REPOSITORY_MODULE so container.get(moduleLoader.token)
resolves the FeaturesRepository class correctly; keep loadModule unchanged.


export type { FeaturesRepository };
6 changes: 4 additions & 2 deletions packages/features/di/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { BOOKING_DI_TOKENS } from "@calcom/features/bookings/di/tokens";
import { BOOKING_AUDIT_DI_TOKENS } from "@calcom/features/booking-audit/di/tokens";
import { FEATURE_OPT_IN_DI_TOKENS } from "@calcom/features/feature-opt-in/di/tokens";
import { FLAGS_DI_TOKENS } from "@calcom/features/flags/di/tokens";
import { HASHED_LINK_DI_TOKENS } from "@calcom/features/hashedLink/di/tokens";
import { OAUTH_DI_TOKENS } from "@calcom/features/oauth/di/tokens";
import { ORGANIZATION_DI_TOKENS } from "@calcom/features/ee/organizations/di/tokens";
Expand Down Expand Up @@ -34,8 +36,8 @@ export const DI_TOKENS = {
INSIGHTS_ROUTING_SERVICE_MODULE: Symbol("InsightsRoutingServiceModule"),
INSIGHTS_BOOKING_SERVICE: Symbol("InsightsBookingService"),
INSIGHTS_BOOKING_SERVICE_MODULE: Symbol("InsightsBookingServiceModule"),
FEATURES_REPOSITORY: Symbol("FeaturesRepository"),
FEATURES_REPOSITORY_MODULE: Symbol("FeaturesRepositoryModule"),
...FLAGS_DI_TOKENS,
...FEATURE_OPT_IN_DI_TOKENS,
CHECK_BOOKING_LIMITS_SERVICE: Symbol("CheckBookingLimitsService"),
CHECK_BOOKING_LIMITS_SERVICE_MODULE: Symbol("CheckBookingLimitsServiceModule"),
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE: Symbol("CheckBookingAndDurationLimitsService"),
Expand Down
4 changes: 4 additions & 0 deletions packages/features/feature-opt-in/di/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const FEATURE_OPT_IN_DI_TOKENS = {
FEATURE_OPT_IN_SERVICE: Symbol("FeatureOptInService"),
FEATURE_OPT_IN_SERVICE_MODULE: Symbol("FeatureOptInServiceModule"),
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { afterEach, describe, expect, it } from "vitest";

import { getFeatureOptInService } from "@calcom/features/di/containers/FeatureOptInService";
import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository";
import type { FeatureId } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { prisma } from "@calcom/prisma";

import { FeatureOptInService } from "./FeatureOptInService";
import type { IFeatureOptInService } from "./IFeatureOptInService";
Comment on lines +3 to +9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use a shared container to avoid repo cache divergence in tests.
getFeaturesRepository() and getFeatureOptInService() create separate containers, so cache clearing on the test repo won’t affect the repo instance inside the service. If the repository caches data (the clearCache usage suggests it does), this can lead to stale reads or flaky tests.

🔧 Suggested change (shared container)
-import { getFeatureOptInService } from "@calcom/features/di/containers/FeatureOptInService";
-import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository";
+import { createContainer } from "@calcom/features/di/di";
+import { moduleLoader as featureOptInServiceModuleLoader } from "@calcom/features/di/modules/FeatureOptInService";
+import { moduleLoader as featuresRepositoryModuleLoader } from "@calcom/features/di/modules/FeaturesRepository";
@@
-  const featuresRepository = getFeaturesRepository();
-  const service = getFeatureOptInService();
+  const container = createContainer();
+  featureOptInServiceModuleLoader.loadModule(container);
+  featuresRepositoryModuleLoader.loadModule(container);
+  const featuresRepository = container.get<FeaturesRepository>(featuresRepositoryModuleLoader.token);
+  const service = container.get<IFeatureOptInService>(featureOptInServiceModuleLoader.token);

Also applies to: 82-84

🤖 Prompt for AI Agents
In
`@packages/features/feature-opt-in/services/FeatureOptInService.integration-test.ts`
around lines 3 - 9, Tests are constructing the FeaturesRepository and
FeatureOptInService from separate DI containers causing cache divergence;
instead obtain both instances from the same container so they share the same
repository/cache. Locate the test setup where getFeaturesRepository() and
getFeatureOptInService() are both called and change it to create or retrieve a
single DI container (or pass the same repo instance into the service), then
resolve both the FeaturesRepository and the IFeatureOptInService from that same
container so clearCache() on the repo affects the service's repo; ensure you
update any calls to getFeatureOptInService to accept or resolve the shared
repo/container (symbols: getFeaturesRepository, getFeatureOptInService,
IFeatureOptInService).


// Helper to generate unique identifiers per test
const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
Expand All @@ -30,7 +32,7 @@ interface TestEntities {
team: { id: number };
team2: { id: number };
featuresRepository: FeaturesRepository;
service: FeatureOptInService;
service: IFeatureOptInService;
createdFeatures: string[];
setupFeature: (enabled?: boolean) => Promise<FeatureId>;
}
Expand Down Expand Up @@ -77,8 +79,8 @@ async function setup(): Promise<TestEntities> {
},
});

const featuresRepository = new FeaturesRepository(prisma);
const service = new FeatureOptInService(featuresRepository);
const featuresRepository = getFeaturesRepository();
const service = getFeatureOptInService();

// Helper to create a feature for a test and track it for cleanup
const setupFeature = async (enabled = true): Promise<FeatureId> => {
Expand Down
16 changes: 2 additions & 14 deletions packages/features/feature-opt-in/services/FeatureOptInService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,13 @@ import type { FeaturesRepository } from "@calcom/features/flags/features.reposit
import { OPT_IN_FEATURES } from "../config";
import { applyAutoOptIn } from "../lib/applyAutoOptIn";
import { computeEffectiveStateAcrossTeams } from "../lib/computeEffectiveState";

type ResolvedFeatureState = {
featureId: FeatureId;
globalEnabled: boolean;
orgState: FeatureState; // Raw state (before auto-opt-in transform)
teamStates: FeatureState[]; // Raw states
userState: FeatureState | undefined; // Raw state
effectiveEnabled: boolean;
// Auto-opt-in flags for UI to show checkbox state
orgAutoOptIn: boolean;
teamAutoOptIns: boolean[];
userAutoOptIn: boolean;
};
import type { IFeatureOptInService, ResolvedFeatureState } from "./IFeatureOptInService";

/**
* Service class for managing feature opt-in logic.
* Computes effective states based on global, org, team, and user settings.
*/
export class FeatureOptInService {
export class FeatureOptInService implements IFeatureOptInService {
constructor(private featuresRepository: FeaturesRepository) {}

/**
Expand Down
39 changes: 39 additions & 0 deletions packages/features/feature-opt-in/services/IFeatureOptInService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { FeatureId, FeatureState } from "@calcom/features/flags/config";

export type ResolvedFeatureState = {
featureId: FeatureId;
globalEnabled: boolean;
orgState: FeatureState; // Raw state (before auto-opt-in transform)
teamStates: FeatureState[]; // Raw states
userState: FeatureState | undefined; // Raw state
effectiveEnabled: boolean;
// Auto-opt-in flags for UI to show checkbox state
orgAutoOptIn: boolean;
teamAutoOptIns: boolean[];
userAutoOptIn: boolean;
};

export interface IFeatureOptInService {
resolveFeatureStatesAcrossTeams(input: {
userId: number;
orgId: number | null;
teamIds: number[];
featureIds: FeatureId[];
}): Promise<Record<string, ResolvedFeatureState>>;
listFeaturesForUser(input: { userId: number; orgId: number | null; teamIds: number[] }): Promise<
ResolvedFeatureState[]
>;
listFeaturesForTeam(
input: { teamId: number }
): Promise<{ featureId: FeatureId; globalEnabled: boolean; teamState: FeatureState }[]>;
setUserFeatureState(
input:
| { userId: number; featureId: FeatureId; state: "enabled" | "disabled"; assignedBy: number }
| { userId: number; featureId: FeatureId; state: "inherit" }
): Promise<void>;
setTeamFeatureState(
input:
| { teamId: number; featureId: FeatureId; state: "enabled" | "disabled"; assignedBy: number }
| { teamId: number; featureId: FeatureId; state: "inherit" }
): Promise<void>;
}
4 changes: 4 additions & 0 deletions packages/features/flags/di/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const FLAGS_DI_TOKENS = {
FEATURES_REPOSITORY: Symbol("FeaturesRepository"),
FEATURES_REPOSITORY_MODULE: Symbol("FeaturesRepositoryModule"),
};
7 changes: 2 additions & 5 deletions packages/trpc/server/routers/viewer/featureOptIn/_router.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { z } from "zod";

import { getFeatureOptInService } from "@calcom/features/di/containers/FeatureOptInService";
import { isOptInFeature } from "@calcom/features/feature-opt-in/config";
import { FeatureOptInService } from "@calcom/features/feature-opt-in/services/FeatureOptInService";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";

import { TRPCError } from "@trpc/server";
Expand All @@ -15,8 +13,7 @@ import { router } from "../../../trpc";

const featureStateSchema = z.enum(["enabled", "disabled", "inherit"]);

const featuresRepository = new FeaturesRepository(prisma);
const featureOptInService = new FeatureOptInService(featuresRepository);
const featureOptInService = getFeatureOptInService();

/**
* Helper to get user's org and team IDs from their memberships.
Expand Down
Loading