From 8d79209e74e955f1df5e6d587734ca7037b14cb4 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:28:46 -0500 Subject: [PATCH] perf: Optmize DB queries around attribute routing (#27600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace n x m array functions with maps * feat: add attributeIds filter to findManyByOrgMembershipIds Allow filtering attribute-to-user assignments by specific attribute IDs. This enables querying only the attributes needed for a routing rule instead of fetching all attributes for all team members. Co-Authored-By: Claude Opus 4.5 * feat: extract attribute IDs from routing rules and filter queries Add extractAttributeIdsFromQueryValue helper to parse routing rules and identify which attributes are referenced. Pass these IDs through getAttributesAssignmentData to _queryAllData to only fetch the attribute assignments that are actually needed for evaluation. For a team with 785 members and 14 attributes, if a routing rule only references 2-3 attributes, this reduces the query from ~3500 rows to ~500 rows. Co-Authored-By: Claude Opus 4.5 * feat: use attribute filtering in findTeamMembersMatchingAttributeLogic Extract attribute IDs from the routing rules (main + fallback) and pass them to getAttributesForLogic to only fetch necessary data. This completes the optimization to reduce database load when evaluating routing rules that only reference a subset of attributes. Co-Authored-By: Claude Opus 4.5 * feat: filter attributes query by attribute IDs Also filter the attributes query (not just attributeToUser assignments) by the attribute IDs referenced in routing rules. For a routing rule that references 2 out of 14 attributes with 6524 total options, this reduces: - Attributes fetched: 14 → 2 - Options fetched: 6524 → ~930 (proportional reduction) - Fewer regex replacements in replaceAttributeOptionIdsWithOptionLabel Co-Authored-By: Claude Opus 4.5 * test: add comprehensive test coverage for attribute routing query optimization Co-Authored-By: joe@cal.com * Revert "Replace n x m array functions with maps" This reverts commit 312eeb03bd1aba19760f093c798861a23b5fc66f. * fix: re-establish extractAttributeIdsFromQueryValue mock after resetAllMocks Co-Authored-By: joe@cal.com --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../attributes/lib/getAttributes.test.ts | 520 +++++++++++++++++- .../features/attributes/lib/getAttributes.ts | 57 +- .../repositories/PrismaAttributeRepository.ts | 8 +- .../PrismaAttributeToUserRepository.ts | 16 +- ...dTeamMembersMatchingAttributeLogic.test.ts | 3 + .../findTeamMembersMatchingAttributeLogic.ts | 24 +- 6 files changed, 620 insertions(+), 8 deletions(-) diff --git a/packages/features/attributes/lib/getAttributes.test.ts b/packages/features/attributes/lib/getAttributes.test.ts index c35c073524..2edb922243 100644 --- a/packages/features/attributes/lib/getAttributes.test.ts +++ b/packages/features/attributes/lib/getAttributes.test.ts @@ -6,7 +6,12 @@ import type { Attribute } from "@calcom/app-store/routing-forms/types/types"; import type { AttributeOption } from "@calcom/prisma/client"; import { AttributeType, MembershipRole } from "@calcom/prisma/enums"; -import { getAttributesForTeam, getAttributesAssignmentData, getUsersAttributes } from "./getAttributes"; +import { + getAttributesForTeam, + getAttributesAssignmentData, + getUsersAttributes, + extractAttributeIdsFromQueryValue, +} from "./getAttributes"; // Helper functions to create test data async function createMockAttribute({ @@ -477,4 +482,517 @@ describe("getAttributes", () => { }); }); }); + + describe("getAttributesAssignmentData with attributeIds filter", () => { + it("should return only assignments for specified attributeIds when filter is provided", async () => { + const team = await createMockTeam({ orgId }); + const { user, orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({ + orgId, + teamId: team.id, + }); + + const attr1 = await createMockAttribute({ + orgId, + id: "attr1", + name: "Department", + slug: "department", + type: AttributeType.SINGLE_SELECT, + options: [ + { id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }, + { id: "dept-sales", value: "Sales", slug: "sales", isGroup: false, contains: [] }, + ], + }); + + const attr2 = await createMockAttribute({ + orgId, + id: "attr2", + name: "Territory", + slug: "territory", + type: AttributeType.SINGLE_SELECT, + options: [ + { id: "territory-india", value: "India", slug: "india", isGroup: false, contains: [] }, + { id: "territory-china", value: "China", slug: "china", isGroup: false, contains: [] }, + ], + }); + + const attr3 = await createMockAttribute({ + orgId, + id: "attr3", + name: "Level", + slug: "level", + type: AttributeType.SINGLE_SELECT, + options: [ + { id: "level-senior", value: "Senior", slug: "senior", isGroup: false, contains: [] }, + { id: "level-junior", value: "Junior", slug: "junior", isGroup: false, contains: [] }, + ], + }); + + await createMockAttributeAssignment({ + orgMembershipId: orgMembership.id, + attributeOptionId: "dept-eng", + }); + + await createMockAttributeAssignment({ + orgMembershipId: orgMembership.id, + attributeOptionId: "territory-india", + }); + + await createMockAttributeAssignment({ + orgMembershipId: orgMembership.id, + attributeOptionId: "level-senior", + }); + + const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({ + teamId: team.id, + orgId, + attributeIds: ["attr1", "attr2"], + }); + + expect(attributesOfTheOrg).toHaveLength(2); + expect(attributesOfTheOrg.map((a) => a.id).sort()).toEqual(["attr1", "attr2"]); + + expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1); + expect(attributesAssignedToTeamMembersWithOptions[0].userId).toBe(user.id); + expect(Object.keys(attributesAssignedToTeamMembersWithOptions[0].attributes)).toEqual(["attr1", "attr2"]); + expect(attributesAssignedToTeamMembersWithOptions[0].attributes.attr1).toBeDefined(); + expect(attributesAssignedToTeamMembersWithOptions[0].attributes.attr2).toBeDefined(); + expect(attributesAssignedToTeamMembersWithOptions[0].attributes.attr3).toBeUndefined(); + }); + + it("should return all assignments when attributeIds is undefined", async () => { + const team = await createMockTeam({ orgId }); + const { user, orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({ + orgId, + teamId: team.id, + }); + + await createMockAttribute({ + orgId, + id: "attr1", + name: "Department", + slug: "department", + type: AttributeType.SINGLE_SELECT, + options: [{ id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }], + }); + + await createMockAttribute({ + orgId, + id: "attr2", + name: "Territory", + slug: "territory", + type: AttributeType.SINGLE_SELECT, + options: [{ id: "territory-india", value: "India", slug: "india", isGroup: false, contains: [] }], + }); + + await createMockAttributeAssignment({ + orgMembershipId: orgMembership.id, + attributeOptionId: "dept-eng", + }); + + await createMockAttributeAssignment({ + orgMembershipId: orgMembership.id, + attributeOptionId: "territory-india", + }); + + const { attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({ + teamId: team.id, + orgId, + }); + + expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1); + expect(Object.keys(attributesAssignedToTeamMembersWithOptions[0].attributes)).toHaveLength(2); + }); + + it("should return all assignments when attributeIds is an empty array (backwards compatible)", async () => { + const team = await createMockTeam({ orgId }); + const { orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({ + orgId, + teamId: team.id, + }); + + await createMockAttribute({ + orgId, + id: "attr1", + name: "Department", + slug: "department", + type: AttributeType.SINGLE_SELECT, + options: [{ id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }], + }); + + await createMockAttributeAssignment({ + orgMembershipId: orgMembership.id, + attributeOptionId: "dept-eng", + }); + + const { attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({ + teamId: team.id, + orgId, + attributeIds: [], + }); + + expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1); + expect(Object.keys(attributesAssignedToTeamMembersWithOptions[0].attributes)).toHaveLength(1); + }); + + it("should return only matching assignments when some attributeIds do not exist", async () => { + const team = await createMockTeam({ orgId }); + const { user, orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({ + orgId, + teamId: team.id, + }); + + await createMockAttribute({ + orgId, + id: "attr1", + name: "Department", + slug: "department", + type: AttributeType.SINGLE_SELECT, + options: [{ id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }], + }); + + await createMockAttributeAssignment({ + orgMembershipId: orgMembership.id, + attributeOptionId: "dept-eng", + }); + + const { attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({ + teamId: team.id, + orgId, + attributeIds: ["attr1", "non-existent-attr"], + }); + + expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1); + expect(Object.keys(attributesAssignedToTeamMembersWithOptions[0].attributes)).toEqual(["attr1"]); + }); + }); +}); + +describe("extractAttributeIdsFromQueryValue", () => { + it("should return empty array for null queryValue", () => { + const result = extractAttributeIdsFromQueryValue(null); + expect(result).toEqual([]); + }); + + it("should return empty array for queryValue with no rules", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: {}, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toEqual([]); + }); + + it("should extract single attribute ID from a single rule", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-123", + operator: "select_equals", + value: ["option-1"], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toEqual(["attr-123"]); + }); + + it("should extract multiple attribute IDs from multiple rules", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-123", + operator: "select_equals", + value: ["option-1"], + }, + }, + "rule-2": { + type: "rule", + properties: { + field: "attr-456", + operator: "select_equals", + value: ["option-2"], + }, + }, + "rule-3": { + type: "rule", + properties: { + field: "attr-789", + operator: "multiselect_some_in", + value: [["option-3", "option-4"]], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toHaveLength(3); + expect(result).toContain("attr-123"); + expect(result).toContain("attr-456"); + expect(result).toContain("attr-789"); + }); + + it("should deduplicate attribute IDs when same attribute is used in multiple rules", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-123", + operator: "select_equals", + value: ["option-1"], + }, + }, + "rule-2": { + type: "rule", + properties: { + field: "attr-123", + operator: "select_not_equals", + value: ["option-2"], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toEqual(["attr-123"]); + }); + + it("should extract attribute IDs from nested children1 structures", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "group-1": { + type: "group", + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-nested-1", + operator: "select_equals", + value: ["option-1"], + }, + }, + }, + }, + "rule-2": { + type: "rule", + properties: { + field: "attr-top-level", + operator: "select_equals", + value: ["option-2"], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toHaveLength(2); + expect(result).toContain("attr-nested-1"); + expect(result).toContain("attr-top-level"); + }); + + it("should extract attribute IDs from both queryValue and fallbackQueryValue", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-main", + operator: "select_equals", + value: ["option-1"], + }, + }, + }, + }; + const fallbackQueryValue = { + id: "query-2", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-fallback", + operator: "select_equals", + value: ["option-2"], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue, fallbackQueryValue); + expect(result).toHaveLength(2); + expect(result).toContain("attr-main"); + expect(result).toContain("attr-fallback"); + }); + + it("should deduplicate attribute IDs across queryValue and fallbackQueryValue", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-shared", + operator: "select_equals", + value: ["option-1"], + }, + }, + }, + }; + const fallbackQueryValue = { + id: "query-2", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-shared", + operator: "select_equals", + value: ["option-2"], + }, + }, + "rule-2": { + type: "rule", + properties: { + field: "attr-unique", + operator: "select_equals", + value: ["option-3"], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue, fallbackQueryValue); + expect(result).toHaveLength(2); + expect(result).toContain("attr-shared"); + expect(result).toContain("attr-unique"); + }); + + it("should handle queryValue with undefined children1", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toEqual([]); + }); + + it("should handle rules without properties", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toEqual([]); + }); + + it("should handle rules with non-string field values", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: 123, + operator: "select_equals", + value: ["option-1"], + }, + }, + "rule-2": { + type: "rule", + properties: { + field: "valid-attr", + operator: "select_equals", + value: ["option-2"], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toEqual(["valid-attr"]); + }); + + it("should handle switch_group type queryValue", () => { + const queryValue = { + id: "query-1", + type: "switch_group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-switch", + operator: "select_equals", + value: ["option-1"], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toEqual(["attr-switch"]); + }); + + it("should handle deeply nested group structures", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "group-1": { + type: "group", + children1: { + "group-2": { + type: "group", + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-deep", + operator: "select_equals", + value: ["option-1"], + }, + }, + }, + }, + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue); + expect(result).toEqual(["attr-deep"]); + }); + + it("should handle null fallbackQueryValue", () => { + const queryValue = { + id: "query-1", + type: "group" as const, + children1: { + "rule-1": { + type: "rule", + properties: { + field: "attr-main", + operator: "select_equals", + value: ["option-1"], + }, + }, + }, + }; + const result = extractAttributeIdsFromQueryValue(queryValue, null); + expect(result).toEqual(["attr-main"]); + }); }); diff --git a/packages/features/attributes/lib/getAttributes.ts b/packages/features/attributes/lib/getAttributes.ts index 742695ddb9..8fd7df6492 100644 --- a/packages/features/attributes/lib/getAttributes.ts +++ b/packages/features/attributes/lib/getAttributes.ts @@ -7,11 +7,54 @@ import type { import { PrismaAttributeRepository } from "@calcom/features/attributes/repositories/PrismaAttributeRepository"; import { PrismaAttributeToUserRepository } from "@calcom/features/attributes/repositories/PrismaAttributeToUserRepository"; import logger from "@calcom/lib/logger"; +import type { AttributesQueryValue } from "@calcom/lib/raqb/types"; import { safeStringify } from "@calcom/lib/safeStringify"; import prisma from "@calcom/prisma"; import type { AttributeToUser } from "@calcom/prisma/client"; import type { AttributeType } from "@calcom/prisma/enums"; +/** + * Extracts attribute IDs referenced in a routing rule's attributesQueryValue. + * This allows us to only fetch the attributes that are actually needed for evaluation. + */ +export function extractAttributeIdsFromQueryValue( + queryValue: AttributesQueryValue | null, + fallbackQueryValue?: AttributesQueryValue | null +): string[] { + const attributeIds = new Set(); + + function walkRules(obj: unknown) { + if (!obj || typeof obj !== "object") return; + + const record = obj as Record; + + // Check if this is a rule with a field property (the attribute ID) + if (record.type === "rule" && record.properties) { + const properties = record.properties as Record; + if (typeof properties.field === "string") { + attributeIds.add(properties.field); + } + } + + // Recursively walk children1 (the RAQB structure for nested rules) + if (record.children1 && typeof record.children1 === "object") { + for (const child of Object.values(record.children1 as Record)) { + walkRules(child); + } + } + } + + if (queryValue) { + walkRules(queryValue); + } + + if (fallbackQueryValue) { + walkRules(fallbackQueryValue); + } + + return Array.from(attributeIds); +} + type UserId = number; type OrgMembershipId = number; type AttributeOptionId = string; @@ -274,9 +317,12 @@ async function _getOrgMembershipToUserIdForTeam({ async function _queryAllData({ orgId, teamId, + attributeIds, }: { orgId: number; teamId: number; + /** If provided, only fetch attribute assignments for these attribute IDs */ + attributeIds?: string[]; }) { const attributeRepo = new PrismaAttributeRepository(prisma); const attributeToUserRepo = new PrismaAttributeToUserRepository(prisma); @@ -284,17 +330,19 @@ async function _queryAllData({ const [orgMembershipToUserIdForTeamMembers, attributesOfTheOrg] = await Promise.all([ _getOrgMembershipToUserIdForTeam({ orgId, teamId }), - attributeRepo.findManyByOrgId({ orgId }), + attributeRepo.findManyByOrgId({ orgId, attributeIds }), ]); const orgMembershipIds = Array.from( orgMembershipToUserIdForTeamMembers.keys() ); - // Get all the attributes assigned to the members of the team + // Get the attributes assigned to the members of the team + // If attributeIds is provided, only fetch assignments for those specific attributes const attributesToUsersForTeam = await attributeToUserRepo.findManyByOrgMembershipIds({ orgMembershipIds, + attributeIds, }); return { @@ -418,9 +466,13 @@ function _buildAssignmentsForTeam({ export async function getAttributesAssignmentData({ orgId, teamId, + attributeIds, }: { orgId: number; teamId: number; + /** If provided, only fetch attribute assignments for these attribute IDs. + * This significantly improves performance when only a few attributes are needed. */ + attributeIds?: string[]; }) { const { attributesOfTheOrg, @@ -429,6 +481,7 @@ export async function getAttributesAssignmentData({ } = await _queryAllData({ orgId, teamId, + attributeIds, }); const assignmentsForTheTeam = _buildAssignmentsForTeam({ diff --git a/packages/features/attributes/repositories/PrismaAttributeRepository.ts b/packages/features/attributes/repositories/PrismaAttributeRepository.ts index aad5a1c9ae..4e14c8b593 100644 --- a/packages/features/attributes/repositories/PrismaAttributeRepository.ts +++ b/packages/features/attributes/repositories/PrismaAttributeRepository.ts @@ -27,11 +27,17 @@ export class PrismaAttributeRepository { }); } - async findManyByOrgId({ orgId }: { orgId: number }) { + async findManyByOrgId({ orgId, attributeIds }: { orgId: number; attributeIds?: string[] }) { // It should be a faster query because of lesser number of attributes record and index on teamId const result = await this.prismaClient.attribute.findMany({ where: { teamId: orgId, + // Only filter by attribute IDs if provided + ...(attributeIds?.length && { + id: { + in: attributeIds, + }, + }), }, select: { id: true, diff --git a/packages/features/attributes/repositories/PrismaAttributeToUserRepository.ts b/packages/features/attributes/repositories/PrismaAttributeToUserRepository.ts index 193822d39e..62b71c5a27 100644 --- a/packages/features/attributes/repositories/PrismaAttributeToUserRepository.ts +++ b/packages/features/attributes/repositories/PrismaAttributeToUserRepository.ts @@ -30,7 +30,13 @@ export class PrismaAttributeToUserRepository { }); } - async findManyByOrgMembershipIds({ orgMembershipIds }: { orgMembershipIds: number[] }) { + async findManyByOrgMembershipIds({ + orgMembershipIds, + attributeIds, + }: { + orgMembershipIds: number[]; + attributeIds?: string[]; + }) { if (!orgMembershipIds.length) { return []; } @@ -40,6 +46,14 @@ export class PrismaAttributeToUserRepository { memberId: { in: orgMembershipIds, }, + // Only filter by attribute IDs if provided + ...(attributeIds?.length && { + attributeOption: { + attributeId: { + in: attributeIds, + }, + }, + }), }, }); diff --git a/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.test.ts b/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.test.ts index d12e1bb52a..728d515366 100644 --- a/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.test.ts +++ b/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.test.ts @@ -16,6 +16,7 @@ import { vi.mock("@calcom/features/attributes/lib/getAttributes", () => { return { getAttributesAssignmentData: vi.fn(), + extractAttributeIdsFromQueryValue: vi.fn().mockReturnValue([]), }; }); @@ -265,6 +266,8 @@ function buildScenarioWhereMainAttributeLogicFails() { describe("findTeamMembersMatchingAttributeLogic", () => { beforeEach(() => { vi.resetAllMocks(); + // Re-establish the mock for extractAttributeIdsFromQueryValue after resetAllMocks + vi.mocked(getAttributesModule.extractAttributeIdsFromQueryValue).mockReturnValue([]); }); it("should return null if the route does not have an attributesQueryValue set", async () => { diff --git a/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.ts b/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.ts index a27fde1338..075571581f 100644 --- a/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.ts +++ b/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.ts @@ -1,6 +1,9 @@ import { acrossQueryValueCompatiblity, raqbQueryValueUtils } from "@calcom/app-store/_utils/raqb/raqbUtils"; import type { Attribute } from "@calcom/app-store/routing-forms/types/types"; -import { getAttributesAssignmentData } from "@calcom/features/attributes/lib/getAttributes"; +import { + extractAttributeIdsFromQueryValue, + getAttributesAssignmentData, +} from "@calcom/features/attributes/lib/getAttributes"; import type { RoutingFormTraceService } from "@calcom/features/routing-trace/domains/RoutingFormTraceService"; import { RaqbLogicResult } from "@calcom/lib/raqb/evaluateRaqbLogic"; import jsonLogic from "@calcom/lib/raqb/jsonLogic"; @@ -435,9 +438,19 @@ async function runFallbackAttributeLogic(data: RunAttributeLogicData, options: R }; } -export async function getAttributesForLogic({ teamId, orgId }: { teamId: number; orgId: number }) { +export async function getAttributesForLogic({ + teamId, + orgId, + attributeIds, +}: { + teamId: number; + orgId: number; + /** If provided, only fetch attribute assignments for these attribute IDs. + * This significantly improves performance when only a few attributes are needed. */ + attributeIds?: string[]; +}) { const [result, ttAttributes] = await asyncPerf(async () => { - return getAttributesAssignmentData({ teamId, orgId }); + return getAttributesAssignmentData({ teamId, orgId, attributeIds }); }); return { @@ -482,6 +495,9 @@ export async function findTeamMembersMatchingAttributeLogic( routeIsFallback, } = data; + // Extract attribute IDs from the routing rules to only fetch necessary data + const attributeIds = extractAttributeIdsFromQueryValue(attributesQueryValue, fallbackAttributesQueryValue); + const { attributesOfTheOrg, teamMembersWithAttributeOptionValuePerAttribute, @@ -489,6 +505,8 @@ export async function findTeamMembersMatchingAttributeLogic( } = await getAttributesForLogic({ teamId, orgId, + // Only pass attributeIds if we found some - otherwise fetch all (backwards compatible) + attributeIds: attributeIds.length > 0 ? attributeIds : undefined, }); const runAttributeLogicOptions = {