* 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>
97 lines
3.4 KiB
TypeScript
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);
|
|
}
|
|
}
|