Files
calendar/packages/features/flags/repositories/CachedTeamFeatureRepository.ts
T
512002aaef refactor: replace FeaturesRepository with DI-based feature repositories (#27200)
* refactor: replace FeaturesRepository with DI-based feature repositories

Migrate from the legacy FeaturesRepository pattern to separate DI-based repositories:
- Add checkIfFeatureIsEnabledGlobally to IFeatureRepository
- Add getTeamsWithFeatureEnabled to ITeamFeatureRepository
- Update CalendarSubscriptionService to use featureRepository, teamFeatureRepository, and userFeatureRepository
- Update SelectedCalendarRepository.findNextSubscriptionBatch to filter by teamIds instead of featureIds
- Update OnboardingPathService to use DI via getFeatureRepository()
- Remove prisma parameter from OnboardingPathService callsites

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: update tests to use new DI-based feature repositories

Update CalendarSubscriptionService and SelectedCalendarRepository tests
to use the new separate repository interfaces:
- featureRepository for global feature checks
- teamFeatureRepository for team-level feature checks
- userFeatureRepository for user-level feature checks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: update API routes to use DI-based feature repositories

Update cron and webhook routes to use the new separate repository
interfaces instead of the combined FeaturesRepository.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: remove prisma arg from getGettingStartedPathWithParams call

The OnboardingPathService method no longer requires a prisma argument
as it now uses DI containers internally.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: update route tests to expect new DI-based feature repositories

Update service instantiation tests to expect featureRepository,
teamFeatureRepository, and userFeatureRepository instead of the
old featuresRepository.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: optimize global feature check and add guard in checkForNewSubscriptions

- Use targeted select for only `enabled` field in checkIfFeatureIsEnabledGlobally
- Add global feature flag guard in checkForNewSubscriptions to avoid unnecessary
  DB queries and API calls when the cache feature is globally disabled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* remove redundant comment

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 09:34:03 +00:00

97 lines
3.4 KiB
TypeScript

import { Memoize, Unmemoize } from "@calcom/features/cache";
import type { TeamFeaturesDto } from "@calcom/lib/dto/TeamFeaturesDto";
import { TeamFeaturesDtoSchema } from "@calcom/lib/dto/TeamFeaturesDto";
import { z } from "zod";
import type { FeatureId, TeamFeatures } from "../config";
import type { ITeamFeatureRepository } from "./PrismaTeamFeatureRepository";
import { booleanSchema } from "./schemas";
const CACHE_PREFIX = "features:team";
const KEY = {
byTeamIdAndFeatureId: (teamId: number, featureId: string): string =>
`${CACHE_PREFIX}:${teamId}:${featureId}`,
autoOptInByTeamId: (teamId: number): string => `${CACHE_PREFIX}:autoOptIn:${teamId}`,
enabledFeatures: (teamId: number): string => `${CACHE_PREFIX}:enabledFeatures:${teamId}`,
};
const teamFeaturesSchema = z.record(z.string(), z.boolean()).nullable();
export class CachedTeamFeatureRepository implements ITeamFeatureRepository {
constructor(private prismaTeamFeatureRepository: ITeamFeatureRepository) {}
@Memoize({
key: KEY.byTeamIdAndFeatureId,
schema: TeamFeaturesDtoSchema,
})
async findByTeamIdAndFeatureId(teamId: number, featureId: FeatureId): Promise<TeamFeaturesDto | null> {
return this.prismaTeamFeatureRepository.findByTeamIdAndFeatureId(teamId, featureId);
}
async findByTeamIdsAndFeatureIds(
teamIds: number[],
featureIds: FeatureId[]
): Promise<Partial<Record<FeatureId, Record<number, TeamFeaturesDto>>>> {
return this.prismaTeamFeatureRepository.findByTeamIdsAndFeatureIds(teamIds, featureIds);
}
@Unmemoize({
keys: (teamId: number, featureId: FeatureId) => [
KEY.byTeamIdAndFeatureId(teamId, featureId),
KEY.enabledFeatures(teamId),
],
})
async upsert(
teamId: number,
featureId: FeatureId,
enabled: boolean,
assignedBy: string
): Promise<TeamFeaturesDto> {
return this.prismaTeamFeatureRepository.upsert(teamId, featureId, enabled, assignedBy);
}
@Unmemoize({
keys: (teamId: number, featureId: FeatureId) => [
KEY.byTeamIdAndFeatureId(teamId, featureId),
KEY.enabledFeatures(teamId),
],
})
async delete(teamId: number, featureId: FeatureId): Promise<void> {
return this.prismaTeamFeatureRepository.delete(teamId, featureId);
}
@Memoize({
key: KEY.autoOptInByTeamId,
schema: booleanSchema,
})
async findAutoOptInByTeamId(teamId: number): Promise<boolean> {
return this.prismaTeamFeatureRepository.findAutoOptInByTeamId(teamId);
}
async findAutoOptInByTeamIds(teamIds: number[]): Promise<Record<number, boolean>> {
return this.prismaTeamFeatureRepository.findAutoOptInByTeamIds(teamIds);
}
@Unmemoize({
keys: (teamId: number) => [KEY.autoOptInByTeamId(teamId)],
})
async setAutoOptIn(teamId: number, enabled: boolean): Promise<void> {
return this.prismaTeamFeatureRepository.setAutoOptIn(teamId, enabled);
}
async checkIfTeamHasFeature(teamId: number, featureId: FeatureId): Promise<boolean> {
return this.prismaTeamFeatureRepository.checkIfTeamHasFeature(teamId, featureId);
}
@Memoize({
key: KEY.enabledFeatures,
schema: teamFeaturesSchema,
})
async getEnabledFeatures(teamId: number): Promise<TeamFeatures | null> {
return this.prismaTeamFeatureRepository.getEnabledFeatures(teamId);
}
async getTeamsWithFeatureEnabled(featureId: FeatureId): Promise<number[]> {
return this.prismaTeamFeatureRepository.getTeamsWithFeatureEnabled(featureId);
}
}