Files
calendar/packages/trpc/server/routers/viewer/admin/getTeamsForFeature.handler.ts
T
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
cef8610925 feat: add enabled column to UserFeatures and TeamFeatures for tri-state semantics (#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>
2025-12-18 07:16:20 -03:00

82 lines
1.9 KiB
TypeScript

import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../types";
import type { TAdminGetTeamsForFeatureSchema } from "./getTeamsForFeature.schema";
type GetTeamsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
prisma: PrismaClient;
};
input: TAdminGetTeamsForFeatureSchema;
};
export const getTeamsForFeatureHandler = async ({ ctx, input }: GetTeamsOptions) => {
const { prisma } = ctx;
const { featureId, limit = 10, cursor, searchTerm } = input;
const whereClause = {
isPlatform: false,
...(searchTerm
? {
OR: [
{ name: { contains: searchTerm, mode: "insensitive" as const } },
{ slug: { contains: searchTerm, mode: "insensitive" as const } },
],
}
: {}),
};
const [teams, assignedTeams] = await Promise.all([
prisma.team.findMany({
where: whereClause,
select: {
id: true,
name: true,
slug: true,
logoUrl: true,
parentId: true,
isOrganization: true,
parent: {
select: {
name: true,
logoUrl: true,
},
},
},
orderBy: {
name: "asc",
},
take: limit + 1,
...(cursor ? { skip: 1, cursor: { id: cursor } } : {}),
}),
prisma.teamFeatures.findMany({
where: {
featureId,
enabled: true,
},
select: {
teamId: true,
},
}),
]);
const assignedTeamIds = new Set(assignedTeams.map((tf) => tf.teamId));
let nextCursor: typeof cursor | undefined = undefined;
if (teams.length > limit) {
const nextItem = teams.pop();
nextCursor = nextItem?.id;
}
return {
teams: teams.map((team) => ({
...team,
hasFeature: assignedTeamIds.has(team.id),
})),
nextCursor,
};
};
export default getTeamsForFeatureHandler;