Files
calendar/packages/features/flags/repositories/CachedUserFeatureRepository.ts
T
Eunjae LeeGitHubunknown <>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
29ec2b9cb6 refactor: split flag repositories into Prisma and Cached layers (#27186)
* refactor: split flag repositories into Prisma and Cached layers

- Rename FeatureRepository to PrismaFeatureRepository (raw DB access)
- Rename TeamFeatureRepository to PrismaTeamFeatureRepository (raw DB access)
- Rename UserFeatureRepository to PrismaUserFeatureRepository (raw DB access)
- Create CachedFeatureRepository with @Memoize wrapping PrismaFeatureRepository
- Create CachedTeamFeatureRepository with @Memoize/@Unmemoize wrapping PrismaTeamFeatureRepository
- Create CachedUserFeatureRepository with @Memoize/@Unmemoize wrapping PrismaUserFeatureRepository
- Update DI tokens, modules, and containers for all 6 repositories
- Update imports in FeatureOptInService and related modules
- Update tests to use new repository structure

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: simplify @Memoize key patterns and delegate batch methods to Prisma

- Use direct function references for @Memoize key (e.g., KEY.all instead of () => KEY.all())
- Simplify batch methods in Cached repositories to delegate to Prisma repository
- Update tests to reflect the new delegation pattern

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: add orderBy to TeamRepository.findAllByParentId for deterministic results

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* test: update TeamRepository test to expect orderBy in findAllByParentId

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: cleanup features repository and add specialized repository methods (#27195)

* refactor: cleanup features repository and add findBySlug, update methods

- Remove unused methods from FeaturesRepository (keep getTeamsWithFeatureEnabled)
- Add findAll(), findBySlug(), update() to IFeatureRepository interface
- Add findAll() with caching to CachedFeatureRepository
- Add findBySlug() with caching to CachedFeatureRepository
- Add update() with Unmemoize to CachedFeatureRepository
- Add checkIfFeatureIsEnabledGlobally() to CachedFeatureRepository
- Update toggleFeatureFlag.handler.ts to use repository instead of raw Prisma
- Add comprehensive unit tests for all new methods

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: update updatedAt timestamp in feature update method

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: move feature check methods to specialized repositories

- Replace getUserFeaturesStatus with two checkIfUserHasFeature calls in bookings page
- Move checkIfTeamHasFeature to PrismaTeamFeatureRepository with pass-through in CachedTeamFeatureRepository
- Move checkIfUserHasFeature and checkIfUserHasFeatureNonHierarchical to PrismaUserFeatureRepository with pass-throughs in CachedUserFeatureRepository
- Add getEnabledFeatures to PrismaTeamFeatureRepository with caching in CachedTeamFeatureRepository
- Keep FeaturesRepository methods as pass-throughs for backward compatibility
- Update test to expect updatedAt in feature update

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: remove getUserFeaturesStatus and unused methods from FeaturesRepository

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* restore comment

* fix: invalidate all-features cache on update and enabledFeatures cache on upsert/delete

- CachedFeatureRepository: Add KEY.all() to @Unmemoize keys in update() to prevent stale findAll() results
- CachedTeamFeatureRepository: Add KEY.enabledFeatures(teamId) to @Unmemoize keys in upsert() and delete() to prevent stale getEnabledFeatures() results

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: add CachedUserFeatureRepository tests

Add comprehensive tests for CachedUserFeatureRepository covering:
- findByUserIdAndFeatureId (cache hit, cache miss, not found)
- findByUserIdAndFeatureIds (empty input, multiple features)
- upsert (with cache invalidation)
- delete (with cache invalidation)
- findAutoOptInByUserId (cache hit, cache miss, not found)
- setAutoOptIn (with cache invalidation)

Co-Authored-By: unknown <>

* test: remove tests for methods removed from FeaturesRepository

Remove integration tests for methods that were intentionally removed:
- getUserFeatureStates
- getTeamsFeatureStates
- getUserAutoOptIn
- getTeamsAutoOptIn
- setUserAutoOptIn
- setTeamAutoOptIn

Co-Authored-By: unknown <>

* avoid N+1 query

* refactor: add select clauses to PrismaFeatureRepository queries

- Add explicit select clauses to findAll, findBySlug, and update methods
- Only fetch fields needed for FeatureDto (slug, enabled, description, type, stale, lastUsedAt, createdAt, updatedAt, updatedBy)
- Update tests to expect select clauses
- Fix UserFeatureRepository test to use findMany mock

Co-Authored-By: unknown <>

* fix bad conflict resolved

* use userId

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-02 14:26:23 +01:00

75 lines
2.7 KiB
TypeScript

import { Memoize, Unmemoize } from "@calcom/features/cache";
import type { UserFeaturesDto } from "@calcom/lib/dto/UserFeaturesDto";
import { UserFeaturesDtoSchema } from "@calcom/lib/dto/UserFeaturesDto";
import type { FeatureId } from "../config";
import type { IUserFeatureRepository } from "./PrismaUserFeatureRepository";
import { booleanSchema } from "./schemas";
const CACHE_PREFIX = "features:user";
const KEY = {
byUserIdAndFeatureId: (userId: number, featureId: string): string =>
`${CACHE_PREFIX}:${userId}:${featureId}`,
autoOptInByUserId: (userId: number): string => `${CACHE_PREFIX}:autoOptIn:${userId}`,
};
export class CachedUserFeatureRepository implements IUserFeatureRepository {
constructor(private prismaUserFeatureRepository: IUserFeatureRepository) {}
@Memoize({
key: KEY.byUserIdAndFeatureId,
schema: UserFeaturesDtoSchema,
})
async findByUserIdAndFeatureId(userId: number, featureId: FeatureId): Promise<UserFeaturesDto | null> {
return this.prismaUserFeatureRepository.findByUserIdAndFeatureId(userId, featureId);
}
async findByUserIdAndFeatureIds(
userId: number,
featureIds: FeatureId[]
): Promise<Partial<Record<FeatureId, UserFeaturesDto>>> {
return this.prismaUserFeatureRepository.findByUserIdAndFeatureIds(userId, featureIds);
}
@Unmemoize({
keys: (userId: number, featureId: FeatureId) => [KEY.byUserIdAndFeatureId(userId, featureId)],
})
async upsert(
userId: number,
featureId: FeatureId,
enabled: boolean,
assignedBy: string
): Promise<UserFeaturesDto> {
return this.prismaUserFeatureRepository.upsert(userId, featureId, enabled, assignedBy);
}
@Unmemoize({
keys: (userId: number, featureId: FeatureId) => [KEY.byUserIdAndFeatureId(userId, featureId)],
})
async delete(userId: number, featureId: FeatureId): Promise<void> {
return this.prismaUserFeatureRepository.delete(userId, featureId);
}
@Memoize({
key: KEY.autoOptInByUserId,
schema: booleanSchema,
})
async findAutoOptInByUserId(userId: number): Promise<boolean> {
return this.prismaUserFeatureRepository.findAutoOptInByUserId(userId);
}
@Unmemoize({
keys: (userId: number) => [KEY.autoOptInByUserId(userId)],
})
async setAutoOptIn(userId: number, enabled: boolean): Promise<void> {
return this.prismaUserFeatureRepository.setAutoOptIn(userId, enabled);
}
async checkIfUserHasFeature(userId: number, slug: string): Promise<boolean> {
return this.prismaUserFeatureRepository.checkIfUserHasFeature(userId, slug);
}
async checkIfUserHasFeatureNonHierarchical(userId: number, slug: string): Promise<boolean> {
return this.prismaUserFeatureRepository.checkIfUserHasFeatureNonHierarchical(userId, slug);
}
}