Files
calendar/packages/features/flags/features.repository.interface.ts
T
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f4248bf20d feat: implement FeatureOptInService (#25805)
* feat: implement FeatureOptInService WIP

* clean up

* feat: consolidate feature repositories and add updateFeatureForUser

- Implement updateFeatureForUser in FeaturesRepository (similar to updateFeatureForTeam)
- Move getUserFeatureState and getTeamFeatureState from PrismaFeatureOptInRepository to FeaturesRepository
- Update FeatureOptInService to use only FeaturesRepository
- Add setUserFeatureState and setTeamFeatureState methods to FeatureOptInService
- Update _router.ts to remove PrismaFeatureOptInRepository usage
- Remove PrismaFeatureOptInRepository.ts and FeatureOptInRepositoryInterface.ts
- Update features.repository.interface.ts and features.repository.mock.ts
- Add integration tests for updateFeatureForUser, getUserFeatureState, getTeamFeatureState
- Update service.integration-test.ts to use FeaturesRepository

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

* refactor: rename updateFeatureForUser to setUserFeatureState

Rename to match the convention used for setTeamFeatureState

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

* refactor: return FeatureState type from getUserFeatureState and getTeamFeatureState

* fix integration tests

* clean up logics

* update services and router

* refactor: change getUserFeatureState and getTeamFeatureState to accept featureIds array

- Renamed getUserFeatureState to getUserFeatureStates
- Renamed getTeamFeatureState to getTeamFeatureStates
- Changed parameter from featureId: string to featureIds: string[]
- Changed return type from FeatureState to Record<string, FeatureState>
- Updated FeatureOptInService to use the new batch methods
- Added tests for querying multiple features in a single call
- Optimized listFeaturesForTeam to fetch all feature states in one query

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

* feat: add getFeatureStateForTeams for batch querying multiple teams

- Added getFeatureStateForTeams method to query a single feature across multiple teams in one call
- Updated FeatureOptInService.resolveFeatureStateAcrossTeams to use the new batch method
- Replaces N+1 queries with a single database query for team states
- Added comprehensive integration tests for the new method

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

* refactor: combine org and team state queries into single call

- Include orgId in the teamIds array passed to getFeatureStateForTeams
- Extract org state and team states from the combined result
- Reduces database queries from 3 to 2 in resolveFeatureStateAcrossTeams

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

* refactor: use team.isOrganization and clarify computeEffectiveState comment

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

* refactor: use MembershipRepository.findAllByUserId with isOrganization

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

* feat: add featureId validation using isOptInFeature type guard

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

* less queries

* add fallback value

* fix type error

* move files

* add autoOptInFeatures column

* use autoOptInFeatures flag within FeatureOptInService

* add setUserAutoOptIn and setTeamAutoOptIn

* fix computeEffectiveState logic

* rewrite computeEffectiveState

* clean up integration tests

* clean up in afterEach

* fix type error

* refactor: use FeaturesRepository methods instead of direct Prisma calls

Replace all manual userFeatures and teamFeatures Prisma operations with
the new setUserFeatureState and setTeamFeatureState repository methods.

Changes include:
- Admin handlers (assignFeatureToTeam, unassignFeatureFromTeam)
- Test fixtures and integration tests
- Playwright fixtures
- Development scripts

This ensures consistent feature flag management through the repository
pattern and supports the new tri-state semantics (enabled/disabled/inherit).

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

* clean up

* fix the logic

* extract some logic into applyAutoOptIn()

* remove wrong code

* refactor: convert setUserFeatureState and setTeamFeatureState to object params with discriminated union

- Convert multiple positional parameters to single object parameter
- Use discriminated union types: assignedBy required for enabled/disabled, omitted for inherit
- Update all callers across repository, service, handlers, fixtures, and tests

* fix type error

* use Promise.all

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-06 16:55:53 +01:00

60 lines
2.4 KiB
TypeScript

import type { FeatureId, FeatureState } from "./config";
/**
* Interface for the core FeaturesRepository.
* This interface defines methods for checking feature flags and team feature access.
*/
export interface IFeaturesRepository {
checkIfFeatureIsEnabledGlobally(slug: FeatureId): Promise<boolean>;
checkIfUserHasFeature(userId: number, slug: string): Promise<boolean>;
getUserFeaturesStatus(userId: number, slugs: string[]): Promise<Record<string, boolean>>;
checkIfUserHasFeatureNonHierarchical(userId: number, slug: string): Promise<boolean>;
checkIfTeamHasFeature(teamId: number, slug: FeatureId): Promise<boolean>;
getTeamsWithFeatureEnabled(slug: FeatureId): Promise<number[]>;
setUserFeatureState(
input:
| { userId: number; featureId: FeatureId; state: "enabled" | "disabled"; assignedBy: string }
| { userId: number; featureId: FeatureId; state: "inherit" }
): Promise<void>;
setTeamFeatureState(
input:
| { teamId: number; featureId: FeatureId; state: "enabled" | "disabled"; assignedBy: string }
| { teamId: number; featureId: FeatureId; state: "inherit" }
): Promise<void>;
/**
* Get user's feature states for multiple features.
* @returns Record<featureId, 'enabled' | 'disabled' | 'inherit'>
*/
getUserFeatureStates(input: {
userId: number;
featureIds: FeatureId[];
}): Promise<Record<string, FeatureState>>;
/**
* Get multiple features' states across multiple teams.
* Optimized for querying many teams for many features.
* @returns Record<featureId, Record<teamId, 'enabled' | 'disabled' | 'inherit'>>
*/
getTeamsFeatureStates(input: {
teamIds: number[];
featureIds: FeatureId[];
}): Promise<Record<string, Record<number, FeatureState>>>;
/**
* Get user's autoOptInFeatures flag.
* @returns Promise<boolean> - True if user has auto opt-in enabled
*/
getUserAutoOptIn(userId: number): Promise<boolean>;
/**
* Get autoOptInFeatures for multiple teams (batch).
* @returns Promise<Record<number, boolean>> - Map of teamId to autoOptInFeatures value
*/
getTeamsAutoOptIn(teamIds: number[]): Promise<Record<number, boolean>>;
/**
* Set user's autoOptInFeatures flag.
*/
setUserAutoOptIn(userId: number, enabled: boolean): Promise<void>;
/**
* Set team's autoOptInFeatures flag.
*/
setTeamAutoOptIn(teamId: number, enabled: boolean): Promise<void>;
}