Skip to content

Commit 9de621e

Browse files
perf: replace O(n*m) array lookups with O(1) Map lookups in attribute routing (#27599)
* Replace n x m array functions with maps * test: add tests and benchmark for Map-based attribute lookup optimization Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * test: add real-world enterprise scenario (14 attrs x 6,530 opts) to benchmark Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * chore: remove benchmark file Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Rename function * Pass built maps to functions --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent b3c3b42 commit 9de621e

2 files changed

Lines changed: 371 additions & 32 deletions

File tree

packages/features/attributes/lib/getAttributes.test.ts

Lines changed: 324 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import prismock from "@calcom/testing/lib/__mocks__/prisma";
2-
3-
import { describe, expect, it, beforeEach } from "vitest";
4-
52
import type { Attribute } from "@calcom/app-store/routing-forms/types/types";
63
import type { AttributeOption } from "@calcom/prisma/client";
74
import { AttributeType, MembershipRole } from "@calcom/prisma/enums";
5+
import { beforeEach, describe, expect, it } from "vitest";
86

97
import {
108
getAttributesForTeam,
@@ -483,6 +481,329 @@ describe("getAttributes", () => {
483481
});
484482
});
485483

484+
describe("Map-based lookup optimization", () => {
485+
it("should correctly resolve attribute options when multiple attributes have many options", async () => {
486+
const team = await createMockTeam({ orgId });
487+
const { user, orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
488+
orgId,
489+
teamId: team.id,
490+
});
491+
492+
// Create multiple attributes with multiple options to test the Map lookup
493+
const attr1 = await createMockAttribute({
494+
orgId,
495+
id: "attr1",
496+
name: "Department",
497+
slug: "department",
498+
type: AttributeType.SINGLE_SELECT,
499+
options: [
500+
{ id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] },
501+
{ id: "dept-sales", value: "Sales", slug: "sales", isGroup: false, contains: [] },
502+
{ id: "dept-marketing", value: "Marketing", slug: "marketing", isGroup: false, contains: [] },
503+
],
504+
});
505+
506+
const attr2 = await createMockAttribute({
507+
orgId,
508+
id: "attr2",
509+
name: "Location",
510+
slug: "location",
511+
type: AttributeType.SINGLE_SELECT,
512+
options: [
513+
{ id: "loc-us", value: "United States", slug: "us", isGroup: false, contains: [] },
514+
{ id: "loc-uk", value: "United Kingdom", slug: "uk", isGroup: false, contains: [] },
515+
{ id: "loc-de", value: "Germany", slug: "de", isGroup: false, contains: [] },
516+
],
517+
});
518+
519+
const attr3 = await createMockAttribute({
520+
orgId,
521+
id: "attr3",
522+
name: "Skills",
523+
slug: "skills",
524+
type: AttributeType.MULTI_SELECT,
525+
options: [
526+
{ id: "skill-js", value: "JavaScript", slug: "javascript", isGroup: false, contains: [] },
527+
{ id: "skill-py", value: "Python", slug: "python", isGroup: false, contains: [] },
528+
{ id: "skill-go", value: "Go", slug: "go", isGroup: false, contains: [] },
529+
],
530+
});
531+
532+
// Assign options from different attributes
533+
await createMockAttributeAssignment({
534+
orgMembershipId: orgMembership.id,
535+
attributeOptionId: "dept-eng",
536+
});
537+
538+
await createMockAttributeAssignment({
539+
orgMembershipId: orgMembership.id,
540+
attributeOptionId: "loc-uk",
541+
});
542+
543+
await createMockAttributeAssignment({
544+
orgMembershipId: orgMembership.id,
545+
attributeOptionId: "skill-js",
546+
});
547+
548+
await createMockAttributeAssignment({
549+
orgMembershipId: orgMembership.id,
550+
attributeOptionId: "skill-py",
551+
});
552+
553+
const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } =
554+
await getAttributesAssignmentData({ teamId: team.id, orgId });
555+
556+
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1);
557+
const userAssignment = attributesAssignedToTeamMembersWithOptions[0];
558+
expect(userAssignment.userId).toBe(user.id);
559+
560+
// Verify each attribute was correctly resolved via Map lookup
561+
expect(userAssignment.attributes.attr1).toEqual({
562+
type: AttributeType.SINGLE_SELECT,
563+
attributeOption: { isGroup: false, value: "Engineering", contains: [] },
564+
});
565+
566+
expect(userAssignment.attributes.attr2).toEqual({
567+
type: AttributeType.SINGLE_SELECT,
568+
attributeOption: { isGroup: false, value: "United Kingdom", contains: [] },
569+
});
570+
571+
expect(userAssignment.attributes.attr3).toEqual({
572+
type: AttributeType.MULTI_SELECT,
573+
attributeOption: [
574+
{ isGroup: false, value: "JavaScript", contains: [] },
575+
{ isGroup: false, value: "Python", contains: [] },
576+
],
577+
});
578+
579+
expectAttributesToMatch(attributesOfTheOrg, [attr1, attr2, attr3]);
580+
});
581+
582+
it("should handle group options with nested contains correctly via Map lookup", async () => {
583+
const team = await createMockTeam({ orgId });
584+
const { orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
585+
orgId,
586+
teamId: team.id,
587+
});
588+
589+
// Create attribute with group option that contains other options
590+
await createMockAttribute({
591+
orgId,
592+
id: "attr1",
593+
name: "Region",
594+
slug: "region",
595+
type: AttributeType.MULTI_SELECT,
596+
options: [
597+
{ id: "region-na", value: "North America", slug: "na", isGroup: false, contains: [] },
598+
{ id: "region-eu", value: "Europe", slug: "eu", isGroup: false, contains: [] },
599+
{ id: "region-apac", value: "APAC", slug: "apac", isGroup: false, contains: [] },
600+
{
601+
id: "region-global",
602+
value: "Global",
603+
slug: "global",
604+
isGroup: true,
605+
contains: ["region-na", "region-eu", "region-apac"],
606+
},
607+
],
608+
});
609+
610+
// Assign the group option
611+
await createMockAttributeAssignment({
612+
orgMembershipId: orgMembership.id,
613+
attributeOptionId: "region-global",
614+
});
615+
616+
const { attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({
617+
teamId: team.id,
618+
orgId,
619+
});
620+
621+
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1);
622+
const userAssignment = attributesAssignedToTeamMembersWithOptions[0];
623+
624+
// Verify the group option's contains array was correctly enriched via Map lookup
625+
expect(userAssignment.attributes.attr1).toEqual({
626+
type: AttributeType.MULTI_SELECT,
627+
attributeOption: {
628+
isGroup: true,
629+
value: "Global",
630+
contains: [
631+
{ id: "region-na", value: "North America", slug: "na" },
632+
{ id: "region-eu", value: "Europe", slug: "eu" },
633+
{ id: "region-apac", value: "APAC", slug: "apac" },
634+
],
635+
},
636+
});
637+
});
638+
639+
it("should handle multiple users with different attribute assignments efficiently", async () => {
640+
const team = await createMockTeam({ orgId });
641+
642+
// Create first user
643+
const user1 = await prismock.user.create({
644+
data: { name: "User 1", email: "user1@test.com" },
645+
});
646+
const orgMembership1 = await prismock.membership.create({
647+
data: {
648+
role: MembershipRole.MEMBER,
649+
disableImpersonation: false,
650+
accepted: true,
651+
teamId: orgId,
652+
userId: user1.id,
653+
},
654+
});
655+
await prismock.membership.create({
656+
data: {
657+
role: MembershipRole.MEMBER,
658+
disableImpersonation: false,
659+
accepted: true,
660+
teamId: team.id,
661+
userId: user1.id,
662+
},
663+
});
664+
665+
// Create second user
666+
const user2 = await prismock.user.create({
667+
data: { name: "User 2", email: "user2@test.com" },
668+
});
669+
const orgMembership2 = await prismock.membership.create({
670+
data: {
671+
role: MembershipRole.MEMBER,
672+
disableImpersonation: false,
673+
accepted: true,
674+
teamId: orgId,
675+
userId: user2.id,
676+
},
677+
});
678+
await prismock.membership.create({
679+
data: {
680+
role: MembershipRole.MEMBER,
681+
disableImpersonation: false,
682+
accepted: true,
683+
teamId: team.id,
684+
userId: user2.id,
685+
},
686+
});
687+
688+
// Create attribute with options
689+
await createMockAttribute({
690+
orgId,
691+
id: "attr1",
692+
name: "Level",
693+
slug: "level",
694+
type: AttributeType.SINGLE_SELECT,
695+
options: [
696+
{ id: "level-junior", value: "Junior", slug: "junior", isGroup: false, contains: [] },
697+
{ id: "level-senior", value: "Senior", slug: "senior", isGroup: false, contains: [] },
698+
{ id: "level-lead", value: "Lead", slug: "lead", isGroup: false, contains: [] },
699+
],
700+
});
701+
702+
// Assign different options to different users
703+
await createMockAttributeAssignment({
704+
orgMembershipId: orgMembership1.id,
705+
attributeOptionId: "level-junior",
706+
});
707+
708+
await createMockAttributeAssignment({
709+
orgMembershipId: orgMembership2.id,
710+
attributeOptionId: "level-senior",
711+
});
712+
713+
const { attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({
714+
teamId: team.id,
715+
orgId,
716+
});
717+
718+
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(2);
719+
720+
// Find each user's assignment
721+
const user1Assignment = attributesAssignedToTeamMembersWithOptions.find((a) => a.userId === user1.id);
722+
const user2Assignment = attributesAssignedToTeamMembersWithOptions.find((a) => a.userId === user2.id);
723+
724+
expect(user1Assignment?.attributes.attr1).toEqual({
725+
type: AttributeType.SINGLE_SELECT,
726+
attributeOption: { isGroup: false, value: "Junior", contains: [] },
727+
});
728+
729+
expect(user2Assignment?.attributes.attr1).toEqual({
730+
type: AttributeType.SINGLE_SELECT,
731+
attributeOption: { isGroup: false, value: "Senior", contains: [] },
732+
});
733+
});
734+
735+
it("should handle empty attributes gracefully", async () => {
736+
const team = await createMockTeam({ orgId });
737+
await createMockUserHavingMembershipWithBothTeamAndOrg({
738+
orgId,
739+
teamId: team.id,
740+
});
741+
742+
// Create attribute with no options
743+
await createMockAttribute({
744+
orgId,
745+
id: "attr1",
746+
name: "Empty Attribute",
747+
slug: "empty-attribute",
748+
type: AttributeType.SINGLE_SELECT,
749+
options: [],
750+
});
751+
752+
const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } =
753+
await getAttributesAssignmentData({ teamId: team.id, orgId });
754+
755+
// No assignments since there are no options to assign
756+
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(0);
757+
expect(attributesOfTheOrg).toHaveLength(1);
758+
});
759+
760+
it("should handle option IDs that don't exist in the org's attributes", async () => {
761+
const team = await createMockTeam({ orgId });
762+
const { orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
763+
orgId,
764+
teamId: team.id,
765+
});
766+
767+
await createMockAttribute({
768+
orgId,
769+
id: "attr1",
770+
name: "Department",
771+
slug: "department",
772+
type: AttributeType.SINGLE_SELECT,
773+
options: [
774+
{ id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] },
775+
],
776+
});
777+
778+
// Create an assignment with a valid option
779+
await createMockAttributeAssignment({
780+
orgMembershipId: orgMembership.id,
781+
attributeOptionId: "dept-eng",
782+
});
783+
784+
// Create an assignment with an invalid option ID (simulating data inconsistency)
785+
await prismock.attributeToUser.create({
786+
data: {
787+
memberId: orgMembership.id,
788+
attributeOptionId: "non-existent-option",
789+
},
790+
});
791+
792+
// The function should handle this gracefully and only return valid assignments
793+
const { attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({
794+
teamId: team.id,
795+
orgId,
796+
});
797+
798+
// Should only have the valid assignment
799+
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1);
800+
expect(attributesAssignedToTeamMembersWithOptions[0].attributes.attr1).toEqual({
801+
type: AttributeType.SINGLE_SELECT,
802+
attributeOption: { isGroup: false, value: "Engineering", contains: [] },
803+
});
804+
});
805+
});
806+
486807
describe("getAttributesAssignmentData with attributeIds filter", () => {
487808
it("should return only assignments for specified attributeIds when filter is provided", async () => {
488809
const team = await createMockTeam({ orgId });

0 commit comments

Comments
 (0)