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);
}
Comment on lines +4 to +8
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's check for moduleLoader.token pattern across DI containers
rg -n "moduleLoader\.token" --type ts packages/features/di/

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

Length of output: 68


🏁 Script executed:

#!/bin/bash
# Now let's read the FeaturesRepository.ts file to check the actual code
cat -n packages/features/di/containers/FeaturesRepository.ts

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

Length of output: 599


🏁 Script executed:

#!/bin/bash
# Also check FeaturesRepository.ts in the di folder (might be at a different location)
find packages/features/di -name "FeaturesRepository.ts" -type f

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

Length of output: 190


🏁 Script executed:

#!/bin/bash
# Let's read the FeaturesRepository module to see the moduleLoader implementation
cat -n packages/features/di/modules/FeaturesRepository.ts

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

Length of output: 1010


🏁 Script executed:

#!/bin/bash
# Let's verify the token values to understand the impact better
rg -n "FLAGS_DI_TOKENS\.(FEATURES_REPOSITORY|FEATURES_REPOSITORY_MODULE)" --type ts packages/features/di/

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

Length of output: 306


🏁 Script executed:

#!/bin/bash
# Check if there are other similar patterns to understand the intended design
rg -n "export const moduleLoader.*ModuleLoader.*=.*{" -A 2 --type ts packages/features/di/modules/ | head -50

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

Length of output: 239


🏁 Script executed:

#!/bin/bash
# Check other moduleLoader patterns to understand the intended design
rg -n "export const moduleLoader" -A 2 --type ts packages/features/di/modules/

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

Length of output: 3820


🏁 Script executed:

#!/bin/bash
# Let's check what bindModuleToClassOnToken does to confirm the registration
rg -n "bindModuleToClassOnToken" -A 15 --type ts packages/features/di/di/ | head -40

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

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Find where bindModuleToClassOnToken is defined
find packages/features/di -name "*.ts" -type f | xargs rg -l "bindModuleToClassOnToken"

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

Length of output: 728


🏁 Script executed:

#!/bin/bash
# Also check the import in FeaturesRepository module
rg -n "bindModuleToClassOnToken" -B 2 -A 10 --type ts packages/features/di/modules/FeaturesRepository.ts

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

Length of output: 786


🏁 Script executed:

#!/bin/bash
# Check the di.ts file to understand bindModuleToClassOnToken's behavior
rg -n "bindModuleToClassOnToken" -A 20 --type ts packages/features/di/di.ts | head -50

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

Length of output: 1721


🏁 Script executed:

#!/bin/bash
# Get the full implementation of bindModuleToClassOnToken
rg -n "export function bindModuleToClassOnToken" -A 40 --type ts packages/features/di/di.ts | tail -50

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

Length of output: 1861


Change moduleLoader.token to token in the FeaturesRepository module.

Line 20 in packages/features/di/modules/FeaturesRepository.ts sets token: moduleToken, but should set token: token. The bindModuleToClassOnToken function registers the FeaturesRepository class with the token parameter, not moduleToken. Using moduleToken in the container helper will cause a runtime error when attempting to retrieve the repository, since the container will look for the wrong token. All other modules in this directory correctly use token in their moduleLoader exports.

🤖 Prompt for AI Agents
In `@packages/features/di/containers/FeaturesRepository.ts` around lines 4 - 8,
The module export in the FeaturesRepository module incorrectly sets token:
moduleToken causing the container lookup (used by getFeaturesRepository which
calls featuresRepositoryModuleLoader.token) to miss the registered binding;
update the FeaturesRepository module (where bindModuleToClassOnToken is used) to
export token: token (the same token parameter used when registering the
FeaturesRepository class) instead of token: moduleToken so the container.get in
getFeaturesRepository resolves the correct 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

Bug: moduleLoader.token should reference token, not moduleToken.

The moduleLoader.token is used by container helpers (like getFeaturesRepository) to resolve the bound class via container.get(moduleLoader.token). Here it's incorrectly set to moduleToken (the module's load token) instead of token (the class binding token).

Compare with FeatureOptInService.ts line 20 which correctly uses token.

🐛 Proposed 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, The
moduleLoader export incorrectly assigns moduleLoader.token to moduleToken;
change it to use the class binding token named token so container resolution
works (i.e., set moduleLoader.token = token). Update the object literal for
moduleLoader (symbol: moduleLoader) to reference token instead of moduleToken;
this aligns behavior with getFeaturesRepository and the pattern used in
FeatureOptInService.ts.


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";

// 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();

Comment on lines +82 to 84
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

Ensure service and repository share the same container.
getFeatureOptInService() and getFeaturesRepository() each build a new container; clearFeaturesCache(featuresRepository) may not clear the cache used by service, causing stale reads or flaky tests.

Consider resolving both from a single container in setup() so the cache clear targets the same instance.

🔧 Example approach (shared container)
-  const featuresRepository = getFeaturesRepository();
-  const service = getFeatureOptInService();
+  // Resolve both from a single DI container so caches are shared
+  const container = createContainer();
+  featureOptInServiceModuleLoader.loadModule(container);
+  featuresRepositoryModuleLoader.loadModule(container);
+  const featuresRepository = container.get<FeaturesRepository>(featuresRepositoryModuleLoader.token);
+  const service = container.get<IFeatureOptInService>(featureOptInServiceModuleLoader.token);
🤖 Prompt for AI Agents
In
`@packages/features/feature-opt-in/services/FeatureOptInService.integration-test.ts`
around lines 82 - 84, The test builds separate containers for
getFeatureOptInService() and getFeaturesRepository(), so
clearFeaturesCache(featuresRepository) may not affect the service's cache;
change setup() to resolve both service and repository from the same DI container
instance (e.g., create or get a single container in setup(), then call
container.getFeatureOptInService() and container.getFeaturesRepository()), and
then call clearFeaturesCache(featuresRepository) on that shared repository so
the service and repository use the same cached instance.

// 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