Skip to content
2 changes: 2 additions & 0 deletions packages/features/eventtypes/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { RecurringEvent } from "@calcom/types/Calendar";
import type { UserProfile } from "@calcom/types/UserProfile";
import type { z } from "zod";
import type { EventType } from "./getEventTypeById";

export type CustomInputParsed = typeof customInputSchema._output;

export type AvailabilityOption = {
Expand Down Expand Up @@ -51,6 +52,7 @@ export type Host = {
groupId: string | null;
location?: HostLocation | null;
};

export type TeamMember = {
value: string;
label: string;
Expand Down
18 changes: 18 additions & 0 deletions packages/features/eventtypes/repositories/eventTypeRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1743,6 +1743,24 @@ export class EventTypeRepository implements IEventTypesRepository {
};
}

async findChildrenByParentId(parentId: number) {
return this.prismaClient.eventType.findMany({
where: { parentId },
select: {
hidden: true,
slug: true,
owner: {
select: {
id: true,
name: true,
email: true,
eventTypes: { select: { slug: true } },
},
},
},
});
}

async findByIdWithParentAndUserId(eventTypeId: number) {
return this.prismaClient.eventType.findUnique({
where: { id: eventTypeId },
Expand Down
182 changes: 182 additions & 0 deletions packages/features/host/repositories/HostRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,188 @@ export class HostRepository {
return { items, nextCursor, hasMore };
}

async findHostsForAvailabilityPaginated({
eventTypeId,
cursor,
limit = 20,
search,
}: {
eventTypeId: number;
cursor?: number;
limit?: number;
search?: string;
}) {
const hosts = await this.prismaClient.host.findMany({
where: {
eventTypeId,
...(cursor && { userId: { gt: cursor } }),
...(search && {
OR: [
{ user: { name: { contains: search, mode: "insensitive" as const } } },
{ user: { email: { contains: search, mode: "insensitive" as const } } },
],
}),
},
take: limit + 1,
select: {
userId: true,
isFixed: true,
priority: true,
weight: true,
scheduleId: true,
groupId: true,
user: {
select: {
name: true,
avatarUrl: true,
timeZone: true,
},
},
},
orderBy: [{ userId: "asc" }],
});

const hasMore = hosts.length > limit;
const items = hasMore ? hosts.slice(0, -1) : hosts;
const nextCursor = hasMore ? items[items.length - 1].userId : undefined;

return { items, nextCursor, hasMore };
}

async findHostsForAssignmentPaginated({
eventTypeId,
cursor,
limit = 20,
search,
memberUserIds,
}: {
eventTypeId: number;
cursor?: number;
limit?: number;
search?: string;
memberUserIds?: number[];
}) {
const userIdFilter = memberUserIds?.length
? cursor
? { in: memberUserIds, gt: cursor }
: { in: memberUserIds }
: cursor
? { gt: cursor }
: undefined;
Comment on lines +206 to +212
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: memberUserIds being an empty array currently disables the filter and returns all hosts. Treat an empty array as a valid filter so callers requesting no members get an empty result set instead of all hosts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/host/repositories/HostRepository.ts, line 206:

<comment>`memberUserIds` being an empty array currently disables the filter and returns all hosts. Treat an empty array as a valid filter so callers requesting no members get an empty result set instead of all hosts.</comment>

<file context>
@@ -142,6 +142,188 @@ export class HostRepository {
+    search?: string;
+    memberUserIds?: number[];
+  }) {
+    const userIdFilter = memberUserIds?.length
+      ? cursor
+        ? { in: memberUserIds, gt: cursor }
</file context>
Suggested change
const userIdFilter = memberUserIds?.length
? cursor
? { in: memberUserIds, gt: cursor }
: { in: memberUserIds }
: cursor
? { gt: cursor }
: undefined;
const userIdFilter = memberUserIds
? cursor
? { in: memberUserIds, gt: cursor }
: { in: memberUserIds }
: cursor
? { gt: cursor }
: undefined;
Fix with Cubic


const hosts = await this.prismaClient.host.findMany({
where: {
eventTypeId,
...(userIdFilter && { userId: userIdFilter }),
...(search && {
OR: [
{ user: { name: { contains: search, mode: "insensitive" as const } } },
{ user: { email: { contains: search, mode: "insensitive" as const } } },
],
}),
},
take: limit + 1,
select: {
userId: true,
isFixed: true,
priority: true,
weight: true,
scheduleId: true,
groupId: true,
user: {
select: {
name: true,
email: true,
avatarUrl: true,
},
},
},
orderBy: [{ userId: "asc" }],
});

const hasMore = hosts.length > limit;
const items = hasMore ? hosts.slice(0, -1) : hosts;
const nextCursor = hasMore ? items[items.length - 1].userId : undefined;

// Only check on the first page to avoid an extra query on every scroll
const hasFixedHosts = !cursor
? (await this.prismaClient.host.count({
where: { eventTypeId, isFixed: true },
take: 1,
})) > 0
: undefined;

return { items, nextCursor, hasMore, hasFixedHosts };
}

async findAllRoundRobinHosts({ eventTypeId }: { eventTypeId: number }) {
return this.prismaClient.host.findMany({
where: {
eventTypeId,
isFixed: false,
},
select: {
userId: true,
weight: true,
user: {
select: {
name: true,
email: true,
avatarUrl: true,
},
},
},
orderBy: [{ userId: "asc" }],
});
}

async findChildrenForAssignmentPaginated({
eventTypeId,
cursor,
limit = 20,
search,
}: {
eventTypeId: number;
cursor?: number;
limit?: number;
search?: string;
}) {
const children = await this.prismaClient.eventType.findMany({
where: {
parentId: eventTypeId,
...(cursor && { id: { gt: cursor } }),
...(search && {
OR: [
{ owner: { name: { contains: search, mode: "insensitive" as const } } },
{ owner: { email: { contains: search, mode: "insensitive" as const } } },
],
}),
},
take: limit + 1,
select: {
id: true,
slug: true,
hidden: true,
owner: {
select: {
id: true,
name: true,
email: true,
username: true,
avatarUrl: true,
},
},
},
orderBy: [{ id: "asc" }],
});

const hasMore = children.length > limit;
const items = hasMore ? children.slice(0, -1) : children;
const nextCursor = hasMore ? items[items.length - 1].id : undefined;

return { items, nextCursor, hasMore };
}

async findHostsWithConferencingCredentials(eventTypeId: number) {
return await this.prismaClient.host.findMany({
where: { eventTypeId },
Expand Down
107 changes: 107 additions & 0 deletions packages/features/membership/repositories/MembershipRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,48 @@ export class MembershipRepository {
});
}

async findRoleByUserIdAndTeamId({ userId, teamId }: { userId: number; teamId: number }) {
return await this.prismaClient.membership.findUnique({
where: {
userId_teamId: {
userId,
teamId,
},
},
select: {
role: true,
},
});
}

async findMembershipsWithUserByTeamId({ teamId }: { teamId: number }) {
return this.prismaClient.membership.findMany({
where: { teamId },
select: {
role: true,
accepted: true,
user: {
select: {
name: true,
avatarUrl: true,
username: true,
id: true,
email: true,
locale: true,
defaultScheduleId: true,
isPlatformManaged: true,
timeZone: true,
eventTypes: {
select: {
slug: true,
},
},
},
},
},
});
}

async findAllMembershipsByUserIdForBilling({ userId }: { userId: number }) {
return this.prismaClient.membership.findMany({
where: { userId },
Expand Down Expand Up @@ -594,6 +636,71 @@ export class MembershipRepository {
return !!pendingInvite;
}

async searchMembers({
teamId,
search,
cursor,
limit,
memberUserIds,
}: {
teamId: number;
search?: string | null;
cursor?: number | null;
limit: number;
memberUserIds?: number[] | null;
}) {
const where: Record<string, unknown> = {
teamId,
accepted: true,
};

const userFilter: Record<string, unknown> = {};

if (search) {
userFilter.OR = [
{ name: { contains: search, mode: "insensitive" } },
{ email: { contains: search, mode: "insensitive" } },
];
}

if (memberUserIds?.length && cursor) {
userFilter.id = { in: memberUserIds, gt: cursor };
} else if (memberUserIds?.length) {
userFilter.id = { in: memberUserIds };
} else if (cursor) {
userFilter.id = { gt: cursor };
}

if (Object.keys(userFilter).length > 0) {
where.user = userFilter;
}

const memberships = await this.prismaClient.membership.findMany({
where,
take: limit + 1,
orderBy: { user: { id: "asc" } },
select: {
user: {
select: {
id: true,
name: true,
email: true,
avatarUrl: true,
username: true,
defaultScheduleId: true,
},
},
role: true,
},
});

const hasMore = memberships.length > limit;
const items = hasMore ? memberships.slice(0, limit) : memberships;
const nextCursor = items.length > 0 ? items[items.length - 1].user.id : undefined;

return { memberships: items, nextCursor, hasMore };
}

/**
* Checks if a user has any team membership (pending or accepted).
* Used during onboarding to detect users who signed up via invite token,
Expand Down
Loading
Loading