Skip to content

Comments

perf: add paginated host endpoints and repository methods for large teams#28156

Draft
joeauyeung wants to merge 6 commits intomainfrom
devin/1771970904-improve-event-type-page-backend
Draft

perf: add paginated host endpoints and repository methods for large teams#28156
joeauyeung wants to merge 6 commits intomainfrom
devin/1771970904-improve-event-type-page-backend

Conversation

@joeauyeung
Copy link
Contributor

@joeauyeung joeauyeung commented Feb 24, 2026

What does this PR do?

This is the backend half of #27371, split for easier review. It adds the server-side infrastructure for paginated host fetching to dramatically improve event type page performance for teams with many hosts (tested with 700+).

This PR is purely additive — it introduces new paginated tRPC endpoints and repository methods without modifying any existing return types or queries. The getEventTypeById optimizations, delta-based saving logic, and findTeamMembersMatchingAttributeLogic pagination all live in the frontend PR #28155.

Key changes

New paginated tRPC endpoints:

  • getHostsForAssignment / getHostsForAvailability – cursor-paginated host queries
  • searchTeamMembers – paginated team member search with text filtering
  • getChildrenForAssignment – paginated managed event children
  • exportHostsForWeights – CSV download of host weights

Repository additions:

  • HostRepository pagination methods (findHostsForAssignmentPaginated, findHostsForAvailabilityPaginated, findAllRoundRobinHosts, findChildrenForAssignmentPaginated)
  • MembershipRepository.searchMembers(), findRoleByUserIdAndTeamId(), findMembershipsWithUserByTeamId()
  • UserRepository.findByIdsWithPagination()
  • EventTypeRepository.findChildrenByParentId()

Minor fix:

  • getHostsWithLocationOptions cursor schema changed from .optional() to .nullish() for consistency with other paginated endpoints

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - internal performance optimization, no public API changes.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

Prerequisites:

Manual testing:

  1. Verify existing event type pages still load (backward compatibility — no existing behavior is changed)
  2. Use the frontend PR branch to test paginated host fetching with 30+ hosts

Human Review Checklist

  • hasMembership behavioral changesearchTeamMembers handler uses hasMembership() which checks accepted: true, whereas the original handler didn't check membership acceptance. Confirm this is intended (more secure – only accepted members can search).
  • MembershipRepository.searchMembers Prisma query – When both memberUserIds and cursor are provided, it uses { in: memberUserIds, gt: cursor } on the same field. Confirm Prisma handles this correctly (should filter to IDs in array AND greater than cursor).
  • UserRepository.findByIdsWithPagination total count – The total is computed with the cursor filter applied, so pagination pages after the first report a shrinking total. Confirm whether callers need the full match count or if per-page count is acceptable.

CI Status

All checks passing:

  • Type Checks: ✅
  • Unit Tests: ✅
  • API v2 Unit Tests: ✅
  • Linters: ✅
  • Security Audit: ✅

⚠️ E2E tests skipped (expected): No ready-for-e2e label. The required check intentionally fails when E2E tests are skipped to prevent merging without E2E validation.

Checklist


Link to Devin run: https://app.devin.ai/sessions/91be6db4b8794a4e8f11e2fa2649a2d5
Requested by: @joeauyeung

…vent type editor

- New cursor-paginated tRPC endpoints: getHostsForAssignment, getHostsForAvailability, searchTeamMembers, getChildrenForAssignment, exportHostsForWeights, getHostsWithLocationOptions
- Delta-based host update support in update.handler.ts (pendingHostChanges, pendingChildrenChanges)
- Repository additions: EventTypeRepository.findChildrenByParentId, HostRepository pagination, MembershipRepository.searchMembers, UserRepository.findByIdsWithPagination
- Remove teamMembers from getEventTypeById initial load
- Shared types: PendingHostChangesInput, PendingChildrenChangesInput, HostUpdateInput

Co-Authored-By: unknown <>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 25 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/features/host/repositories/HostRepository.ts">

<violation number="1" location="packages/features/host/repositories/HostRepository.ts:206">
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.</violation>
</file>

<file name="packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts">

<violation number="1" location="packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts:1000">
P1: Performance regression and behavioral change: `findChildrenByParentId` DB query is executed on **every** event type update where `children` is not explicitly provided, even when `pendingChildrenChanges` is also undefined. This adds an unnecessary DB round-trip for non-managed event types and changes the semantics for `updateChildrenEventTypes` (previously received `undefined` meaning "no changes", now receives a fully reconstructed children array).

Guard this block to only execute when there are actually pending children changes to process.</violation>
</file>

<file name="packages/features/users/repositories/UserRepository.ts">

<violation number="1" location="packages/features/users/repositories/UserRepository.ts:471">
P2: `total` is computed with the cursor filter applied, so pagination pages after the first report a shrinking total. If `total` is meant to represent the full match count, compute it without the cursor constraint.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +206 to +212
const userIdFilter = memberUserIds?.length
? cursor
? { in: memberUserIds, gt: cursor }
: { in: memberUserIds }
: cursor
? { gt: cursor }
: undefined;
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 items = hasMore ? users.slice(0, limit) : users;
const nextCursor = hasMore ? items[items.length - 1].id : undefined;

const total = await this.prismaClient.user.count({ where });
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: total is computed with the cursor filter applied, so pagination pages after the first report a shrinking total. If total is meant to represent the full match count, compute it without the cursor constraint.

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

<comment>`total` is computed with the cursor filter applied, so pagination pages after the first report a shrinking total. If `total` is meant to represent the full match count, compute it without the cursor constraint.</comment>

<file context>
@@ -427,6 +427,52 @@ export class UserRepository {
+    const items = hasMore ? users.slice(0, limit) : users;
+    const nextCursor = hasMore ? items[items.length - 1].id : undefined;
+
+    const total = await this.prismaClient.user.count({ where });
+
+    return { users: items, nextCursor, total };
</file context>
Fix with Cubic

@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

Devin AI is addressing Cubic AI's review feedback

New feedback has been sent to the existing Devin session.

View Devin Session


✅ Pushed commit 41227fb

@devin-ai-integration
Copy link
Contributor

Review annotations from original PR #27371

These are the review annotations from @joeauyeung on the original PR, carried over to this split PR for reviewer context.


packages/features/eventtypes/lib/getEventTypeById.ts

On removing async calls in a loop for team memberships:

Making async calls in a loop wrecks performance. Especially for large event types. Since we're paginating team members we don't need to make these calls.

On removing children flattening/profiling:

Since we're paginating the children event types we don't need to fetch this data and iterate through it

On replacing currentUserMembership lookup with repository call:

Fetch the current user's membership to determine if they have permission to delete the event type

On new getEventTypeByIdWithTeamMembers wrapper:

This wrapper around getEventTypeById still fetches memberships since atoms expect membership data along with the event type data.


packages/features/eventtypes/repositories/eventTypeRepository.ts

On changes in this file:

The changes in this file only affect getEventTypeById


Review comments from @eunjae-lee (for reference)

On packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts — direct Prisma usage:

can we use a repository instead of direct prisma usage

On packages/trpc/server/routers/viewer/eventTypes/searchTeamMembers.handler.ts — direct Prisma usage:

let's move this to a repository

On packages/trpc/server/routers/viewer/attributes/findTeamMembersMatchingAttributeLogic.handler.ts — in-memory filtering:

can we update findByIds or create a new method that supports search & limit & cursor out of the box, so that we don't fetch everything and filter at the app level?


Overall review from @eunjae-lee:

massive job ! overall the code looks good. left some comments. let me test it and will leave more review.

}

const { locations, metadata, ...restEventType } = rawEventType;
const newMetadata = eventTypeMetaDataSchemaWithTypedApps.parse(metadata || {}) || {};
Copy link
Contributor

Choose a reason for hiding this comment

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

Making async calls in a loop wrecks performance. Especially for large event types. Since we're paginating team members we don't need to make these calls.

}
: []
),
childrenCount: rawEventType._count?.children ?? 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're paginating the children event types we don't need to fetch this data and iterate through it

const currentUserMembership = eventTypeObject.team?.members.find((el) => el.user.id === userId) ?? null;
// Find the current user's membership role to enable/disable deletion in the UI.
// Null when not a team event type.
const membershipRepo = new MembershipRepository(prisma);
Copy link
Contributor

Choose a reason for hiding this comment

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

Fetch the current user's membership to determine if they have permission to delete the event type

});
}

export const getEventTypeByIdWithTeamMembers = async (props: getEventTypeByIdProps) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This wrapper around getEventTypeById still fetches memberships since atoms expect membership data along with the event type data.

},
},
hosts: {
_count: {
Copy link
Contributor

Choose a reason for hiding this comment

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

The changes in this file only affect getEventTypeById

eventTypeColor,
users,
children,
pendingChildrenChanges,
Copy link
Contributor

Choose a reason for hiding this comment

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

@eunjae-lee's review comment from PR #27371: can we use a repository instead of direct prisma usage

hasMore: boolean;
};

export const searchTeamMembersHandler = async ({
Copy link
Contributor

Choose a reason for hiding this comment

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

@eunjae-lee's review comment from PR #27371: let's move this to a repository

const matchingTeamMembers = await new UserRepository(ctx.prisma).findByIds({ ids: matchingTeamMembersIds });
const userRepo = new UserRepository(ctx.prisma);

const { users, nextCursor, total } = await userRepo.findByIdsWithPagination({
Copy link
Contributor

Choose a reason for hiding this comment

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

@eunjae-lee's review comment from PR #27371: can we update findByIds or create a new method that supports search & limit & cursor out of the box, so that we don't fetch everything and filter at the app level?

@joeauyeung
Copy link
Contributor Author

@Udit-takkar @eunjae-lee @volnei here's the 1st PR in the stack for improving the event type page. I had Devin move all my inline comments here

Move pendingHostChanges/pendingChildrenChanges processing out of backend PR.
These changes belong in the frontend PR since they are tightly coupled
to the new frontend delta tracking components.

Backend PR now contains only read-side optimizations:
- Paginated host/children/member endpoints
- Repository methods
- getEventTypeById optimizations

Co-Authored-By: unknown <>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts">

<violation number="1">
P2: O(n×m) membership check: `teamMemberIds` is an array, and `Array.includes()` is O(m) per host. With 700+ team members (the target scale of this PR), convert to a `Set` for O(1) lookups.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 25, 2026

Devin AI is addressing Cubic AI's review feedback

New feedback has been sent to the existing Devin session.

View Devin Session


✅ No changes pushed

@devin-ai-integration devin-ai-integration bot changed the title perf: add paginated host endpoints and delta-based host updates for event type editor perf: add paginated host endpoints and optimize getEventTypeById for large teams Feb 25, 2026
Reverts getEventTypeById.ts, eventTypeRepository.ts, API v2 atom service,
and platform libraries to main. The backend PR now only adds new
infrastructure (paginated endpoints, repository methods, findChildrenByParentId)
without changing existing return types. The getEventTypeById optimizations
will be in the frontend PR instead.

Co-Authored-By: unknown <>
…ontend PR

The handler's return type change (adding nextCursor/total) breaks frontend
files on main that expect the old shape. Moving these changes to the
frontend PR keeps the backend PR purely additive.

Co-Authored-By: unknown <>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 4 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/features/eventtypes/lib/getEventTypeById.ts">

<violation number="1">
P2: This performs a linear search through team members for every child, which can become O(n²) on large teams. Precompute a memberRoleByUserId map once and use O(1) lookups inside the child mapping.</violation>

<violation number="2">
P2: This loop awaits each owner enrichment sequentially. For many managed children, this adds unnecessary latency; use Promise.all to run enrichments concurrently.</violation>

<violation number="3">
P2: This per-member profile enrichment runs sequentially in a loop, which adds avoidable latency for large teams. Parallelize with Promise.all to keep the per-member lookups concurrent.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Contributor

Devin AI is addressing Cubic AI's review feedback

New feedback has been sent to the existing Devin session.

View Devin Session

@devin-ai-integration
Copy link
Contributor

The three Cubic violations are all in packages/features/eventtypes/lib/getEventTypeById.ts, which was reverted to main on this backend branch (zero diff from main) as part of restructuring. These getEventTypeById changes now live in the frontend PR #28155.\n\nNo changes needed on this PR.\n\n---\n:white_check_mark: No changes pushed

@devin-ai-integration devin-ai-integration bot changed the title perf: add paginated host endpoints and optimize getEventTypeById for large teams perf: add paginated host endpoints and repository methods for large teams Feb 25, 2026
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 2 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/trpc/server/routers/viewer/attributes/findTeamMembersMatchingAttributeLogic.handler.ts">

<violation number="1">
P2: This handler now loads full `userSelect` records even though it only returns `id/name/email`, which pulls sensitive/large columns unnecessarily. Use a minimal select for this lookup to avoid extra data exposure and DB overhead.</violation>

<violation number="2">
P2: Pagination/search support is removed from this handler, so it now always loads all matching users and returns an unbounded payload. This is a behavior regression and can hurt performance on large teams.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Contributor

Devin AI is addressing Cubic AI's review feedback

New feedback has been sent to the existing Devin session.

View Devin Session

@devin-ai-integration
Copy link
Contributor

Both Cubic violations on findTeamMembersMatchingAttributeLogic.handler.ts have confidence 6/10 (below the 9/10 threshold for fixes). Additionally, this file was reverted to match main on the backend branch — the pagination/search changes now live in the frontend PR #28155. No changes needed.


✅ No changes pushed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant