fix: enable DI for FeatureOptInService (#26061)
* fix integration test * fix: enable DI for FeatureOptInService * create containers/FeaturesRepository.ts * refactor: convert FeatureOptInService and FeaturesRepository to moduleLoader pattern - Create feature-specific tokens in feature-opt-in/di/tokens.ts and flags/di/tokens.ts - Update modules to use bindModuleToClassOnToken for type-safe dependency injection - Simplify containers to use moduleLoader.loadModule() for automatic dependency loading - Import feature-specific tokens in central tokens.ts using spread operator Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: rename FeatureOptInServiceInterface to IFeatureOptInService Follow the codebase convention of using 'I' prefix for interface files and names. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: export featuresRepositoryModule for backward compatibility The FeaturesRepository module was refactored to use moduleLoader pattern but AvailableSlots.ts still imports featuresRepositoryModule. This adds the export to maintain backward compatibility. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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";
|
||||
@@ -28,4 +28,3 @@ export const moduleLoader = {
|
||||
token,
|
||||
loadModule
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { IFeatureOptInService } from "@calcom/features/feature-opt-in/services/IFeatureOptInService";
|
||||
|
||||
import { createContainer } from "../di";
|
||||
import { moduleLoader as featureOptInServiceModuleLoader } from "../modules/FeatureOptInService";
|
||||
|
||||
const featureOptInServiceContainer = createContainer();
|
||||
|
||||
export function getFeatureOptInService(): IFeatureOptInService {
|
||||
featureOptInServiceModuleLoader.loadModule(featureOptInServiceContainer);
|
||||
return featureOptInServiceContainer.get<IFeatureOptInService>(featureOptInServiceModuleLoader.token);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createContainer } from "../di";
|
||||
import { type FeaturesRepository, moduleLoader as featuresRepositoryModuleLoader } from "../modules/FeaturesRepository";
|
||||
|
||||
const featuresRepositoryContainer = createContainer();
|
||||
|
||||
export function getFeaturesRepository(): FeaturesRepository {
|
||||
featuresRepositoryModuleLoader.loadModule(featuresRepositoryContainer);
|
||||
return featuresRepositoryContainer.get<FeaturesRepository>(featuresRepositoryModuleLoader.token);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DI_TOKENS } from "@calcom/features/di/tokens";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
import { type Container, createModule } from "../di";
|
||||
|
||||
export const featuresRepositoryModule = createModule();
|
||||
const token = DI_TOKENS.FEATURES_REPOSITORY;
|
||||
const moduleToken = DI_TOKENS.FEATURES_REPOSITORY_MODULE;
|
||||
featuresRepositoryModule.bind(token).toClass(FeaturesRepository, [DI_TOKENS.PRISMA_CLIENT]);
|
||||
|
||||
export const moduleLoader = {
|
||||
token,
|
||||
loadModule: (container: Container) => {
|
||||
container.load(moduleToken, featuresRepositoryModule);
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
loadModule,
|
||||
};
|
||||
|
||||
export type { FeaturesRepository };
|
||||
@@ -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";
|
||||
@@ -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"),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const FEATURE_OPT_IN_DI_TOKENS = {
|
||||
FEATURE_OPT_IN_SERVICE: Symbol("FeatureOptInService"),
|
||||
FEATURE_OPT_IN_SERVICE_MODULE: Symbol("FeatureOptInServiceModule"),
|
||||
};
|
||||
@@ -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)}`;
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const FLAGS_DI_TOKENS = {
|
||||
FEATURES_REPOSITORY: Symbol("FeaturesRepository"),
|
||||
FEATURES_REPOSITORY_MODULE: Symbol("FeaturesRepositoryModule"),
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user