Skip to content

Commit cef8610

Browse files
feat: add enabled column to UserFeatures and TeamFeatures for tri-state semantics (calcom#25765)
* feat: add enabled column to UserFeatures and TeamFeatures for tri-state semantics - Add enabled Boolean column to UserFeatures model with default true - Add enabled Boolean column to TeamFeatures model with default true - Update FeaturesRepository to use tri-state semantics: - enabled=true: feature is explicitly enabled - enabled=false: feature is explicitly disabled (blocks inheritance) - No row: inherit from team/org level - Update SQL queries to check enabled=true for feature access - Add enableFeatureForTeam method to interface and implementation Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * update comments * add integration tests * add more test * select enabled only * no @default(true) * fix types and tests * add missing enabled * add missing enabled * rename enableFeatureForTeam to updateFeatureForTeam and support FeatureState * refactor: rename updateFeatureForTeam to setTeamFeatureState Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix integration test * fix tests * add more tests * add missing enabled --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 056922b commit cef8610

27 files changed

Lines changed: 871 additions & 93 deletions

apps/api/v2/src/modules/organizations/roles/organizations-roles.controller.e2e-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe("Organizations Roles Endpoints", () => {
112112
});
113113

114114
await featuresRepositoryFixture.create({ slug: "pbac", enabled: true });
115-
await featuresRepositoryFixture.enableFeatureForTeam(pbacEnabledOrganization.id, "pbac");
115+
await featuresRepositoryFixture.setTeamFeatureState(pbacEnabledOrganization.id, "pbac", "enabled");
116116

117117
// Create memberships
118118
await membershipRepositoryFixture.create({

apps/api/v2/src/modules/organizations/roles/permissions/organizations-roles-permissions.controller.e2e-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe("Organizations Roles Permissions Endpoints", () => {
5858
});
5959

6060
await featuresRepositoryFixture.create({ slug: "pbac", enabled: true });
61-
await featuresRepositoryFixture.enableFeatureForTeam(pbacEnabledOrganization.id, "pbac");
61+
await featuresRepositoryFixture.setTeamFeatureState(pbacEnabledOrganization.id, "pbac", "enabled");
6262

6363
// Create user + membership in org
6464
pbacOrgUserWithRolePermission = await userRepositoryFixture.create({

apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.controller.e2e-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe("Organizations Roles Endpoints", () => {
126126
});
127127

128128
await featuresRepositoryFixture.create({ slug: "pbac", enabled: true });
129-
await featuresRepositoryFixture.enableFeatureForTeam(pbacEnabledTeam.id, "pbac");
129+
await featuresRepositoryFixture.setTeamFeatureState(pbacEnabledTeam.id, "pbac", "enabled");
130130

131131
// Create memberships
132132
await membershipRepositoryFixture.create({

apps/api/v2/src/modules/organizations/teams/roles/permissions/organizations-teams-roles-permissions.controller.e2e-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe("Organizations Teams Roles Permissions Endpoints", () => {
6767
});
6868

6969
await featuresRepositoryFixture.create({ slug: "pbac", enabled: true });
70-
await featuresRepositoryFixture.enableFeatureForTeam(pbacEnabledTeam.id, "pbac");
70+
await featuresRepositoryFixture.setTeamFeatureState(pbacEnabledTeam.id, "pbac", "enabled");
7171

7272
// Create user + membership in org
7373
pbacOrgUserWithRolePermission = await userRepositoryFixture.create({

apps/api/v2/test/fixtures/repository/features.repository.fixture.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,38 @@ export class FeaturesRepositoryFixture {
2828
});
2929
}
3030

31-
async enableFeatureForTeam(teamId: number, featureId: string, assignedBy = "test") {
32-
await this.prismaWriteClient.teamFeatures.upsert({
33-
where: {
34-
teamId_featureId: {
31+
async setTeamFeatureState(
32+
teamId: number,
33+
featureId: string,
34+
state: "enabled" | "disabled" | "inherit",
35+
assignedBy = "test"
36+
) {
37+
if (state === "enabled" || state === "disabled") {
38+
await this.prismaWriteClient.teamFeatures.upsert({
39+
where: {
40+
teamId_featureId: {
41+
teamId,
42+
featureId,
43+
},
44+
},
45+
create: {
3546
teamId,
3647
featureId,
48+
assignedBy,
49+
enabled: state === "enabled",
3750
},
38-
},
39-
create: {
40-
teamId,
41-
featureId,
42-
assignedBy,
43-
},
44-
update: {},
45-
});
51+
update: {
52+
enabled: state === "enabled",
53+
},
54+
});
55+
} else if (state === "inherit") {
56+
await this.prismaWriteClient.teamFeatures.deleteMany({
57+
where: {
58+
teamId,
59+
featureId,
60+
},
61+
});
62+
}
4663
}
4764

4865
async disableFeatureForTeam(teamId: number, featureSlug: string) {

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import SettingsLayoutAppDirClient from "./SettingsLayoutAppDirClient";
2020
const getTeamFeatures = unstable_cache(
2121
async (teamId: number) => {
2222
const featuresRepository = new FeaturesRepository(prisma);
23-
return await featuresRepository.getTeamFeatures(teamId);
23+
return await featuresRepository.getEnabledTeamFeatures(teamId);
2424
},
2525
["team-features"],
2626
{

apps/web/playwright/lib/test-helpers/pbac.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const enablePBACForTeam = async (teamId: number) => {
2424
teamId: teamId,
2525
assignedBy: "e2e",
2626
assignedAt: new Date(),
27+
enabled: true,
2728
},
2829
});
2930
};

packages/features/ee/teams/repositories/TeamRepository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ export class TeamRepository {
556556
FROM "Membership" m
557557
INNER JOIN "User" u ON m."userId" = u.id
558558
LEFT JOIN "Role" r ON m."customRoleId" = r.id
559-
LEFT JOIN "TeamFeatures" f ON m."teamId" = f."teamId" AND f."featureId" = 'pbac'
559+
LEFT JOIN "TeamFeatures" f ON m."teamId" = f."teamId" AND f."featureId" = 'pbac' AND f.enabled = true
560560
WHERE m."teamId" = ${teamId}
561561
AND m."accepted" = true
562562
AND (

packages/features/flags/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ export type AppFlags = {
3737
};
3838

3939
export type TeamFeatures = Record<keyof AppFlags, boolean>;
40+
41+
export type FeatureState = "enabled" | "disabled" | "inherit";

0 commit comments

Comments
 (0)