Files
calendar/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.integration-test.ts
T
RomitGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
cfb1489e01 perf: batch DB operations in createAttributesScenario to avoid CI timeout (#28320)
* perf: batch DB operations in performance test setup to avoid CI timeout

Replace ~5200 sequential DB inserts with batch createMany() operations
in createHugeAttributesOfTypeSingleSelect. The test was timing out at
60s on 2-vCPU CI runners because the setup phase alone (creating 400
users, 800 memberships, 4000 attribute assignments one-by-one) exceeded
the timeout before the actual logic under test even ran.

Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com>

* perf: optimize createAttributesScenario with batch DB operations

Move batch DB operations (createMany) into the shared createAttributesScenario
helper so all tests benefit, not just the performance test. Simplify
createHugeAttributesOfTypeSingleSelect to delegate to the optimized helper.

Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com>

* Revert "perf: optimize createAttributesScenario with batch DB operations"

This reverts commit c739edd3718796d61f6510ea3a9155065b69a1ce.

* Revert "perf: batch DB operations in performance test setup to avoid CI timeout"

This reverts commit ff6642ae49190c42dd8673a12ee11c8c971370b4.

* perf: optimize createAttributesScenario with batch DB operations

Replace sequential prisma.*.create() calls in the shared createAttributesScenario
helper with batch createMany() calls, reducing ~5,200 sequential DB operations to
~10 batch operations. Simplify createHugeAttributesOfTypeSingleSelect to delegate
to the optimized helper instead of duplicating batch logic.

Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com>

* fix: preserve deterministic user ordering in createAttributesScenario

Map users by email after findMany to ensure the index-based attribute
assignment matches the original userDataList order, since findMany does
not guarantee return order.

Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com>

* revert: make test timeout back to 1 min

* refactor: extract batch helpers createTestUsers, createTestMembershipsForUsers, createTestAttributeAssignments

Organize batch DB operations from createAttributesScenario into
reusable helper functions mirroring the existing single-record helpers.

Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com>

* cleanup: remove unused single-record helpers and JSDoc comments

Remove createTestUser, createTestMemberships, createTestAttributeAssignment
(now replaced by batch versions). Remove added comments.

Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com>

* chore

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-03-08 13:12:25 +05:30

2262 lines
72 KiB
TypeScript

import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
import { RaqbLogicResult } from "@calcom/lib/raqb/evaluateRaqbLogic";
import prisma from "@calcom/prisma";
import type { Team } from "@calcom/prisma/client";
import { AttributeType } from "@calcom/prisma/enums";
import { RoutingFormFieldType } from "@calcom/routing-forms/lib/FieldTypes";
import type { AttributesQueryValue, FormFieldsQueryValue } from "@calcom/routing-forms/types/types";
import type { BaseWidget } from "react-awesome-query-builder";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
findTeamMembersMatchingAttributeLogic,
TroubleshooterCase,
} from "./findTeamMembersMatchingAttributeLogic";
let testFixtures: {
org: Team;
team: Team;
};
const createdResources: {
attributeToUsers: string[];
attributes: string[];
memberships: number[];
users: number[];
} = {
attributeToUsers: [],
attributes: [],
memberships: [],
users: [],
};
const createTestOrganization = async () => {
const timestamp = Date.now() + Math.random();
const org = await prisma.team.create({
data: {
name: `Test Org ${timestamp}`,
slug: `test-org-${timestamp}`,
isOrganization: true,
},
});
return org;
};
const createTestTeam = async (overrides: { parentId: number }) => {
const timestamp = Date.now() + Math.random();
const team = await prisma.team.create({
data: {
name: `Test Team ${timestamp}`,
slug: `test-team-${timestamp}`,
parentId: overrides.parentId,
},
});
return team;
};
const createTestAttribute = async (params: {
orgId: number;
id: string;
name: string;
type: AttributeType;
slug: string;
options: { id: string; value: string; slug: string; isGroup?: boolean; contains?: string[] }[];
}) => {
const attribute = await prisma.attribute.create({
data: {
id: params.id,
teamId: params.orgId,
name: params.name,
type: params.type,
slug: params.slug,
enabled: true,
options: {
createMany: {
data: params.options.map((opt) => ({
id: opt.id,
value: opt.value,
slug: opt.slug,
isGroup: opt.isGroup ?? false,
contains: opt.contains ?? [],
})),
},
},
},
});
createdResources.attributes.push(attribute.id);
return attribute;
};
const createTestUsers = async (userDataList: { email: string; username: string }[]) => {
await prisma.user.createMany({ data: userDataList });
const users = await prisma.user.findMany({
where: { email: { in: userDataList.map((u) => u.email) } },
select: { id: true, email: true },
});
const emailToUser = new Map(users.map((u) => [u.email, u]));
const orderedUsers = userDataList.map((ud) => emailToUser.get(ud.email)!);
createdResources.users.push(...orderedUsers.map((u) => u.id));
return orderedUsers;
};
const createTestMembershipsForUsers = async (params: {
userIds: number[];
orgId: number;
teamId: number;
}) => {
const { userIds, orgId, teamId } = params;
await prisma.membership.createMany({
data: userIds.map((userId) => ({
userId,
teamId: orgId,
role: "MEMBER" as const,
accepted: true,
})),
});
const orgMemberships = await prisma.membership.findMany({
where: { userId: { in: userIds }, teamId: orgId },
select: { id: true, userId: true },
});
createdResources.memberships.push(...orgMemberships.map((m) => m.id));
await prisma.membership.createMany({
data: userIds.map((userId) => ({
userId,
teamId,
role: "MEMBER" as const,
accepted: true,
})),
});
const teamMemberships = await prisma.membership.findMany({
where: { userId: { in: userIds }, teamId },
select: { id: true },
});
createdResources.memberships.push(...teamMemberships.map((m) => m.id));
const userIdToOrgMembershipId = new Map(orgMemberships.map((m) => [m.userId, m.id]));
return { userIdToOrgMembershipId };
};
const createTestAttributeAssignments = async (params: {
orderedUserIds: number[];
userIdToOrgMembershipId: Map<number, number>;
membersAttributes: Record<string, string | string[]>[];
optionValueToId: Map<string, string>;
}) => {
const { orderedUserIds, userIdToOrgMembershipId, membersAttributes, optionValueToId } = params;
const assignments: { memberId: number; attributeOptionId: string }[] = [];
for (let i = 0; i < orderedUserIds.length; i++) {
const orgMembershipId = userIdToOrgMembershipId.get(orderedUserIds[i]);
if (!orgMembershipId) continue;
for (const [attrId, value] of Object.entries(membersAttributes[i])) {
const values = Array.isArray(value) ? value : [value];
for (const v of values) {
const optionId = optionValueToId.get(`${attrId}:${v}`);
if (optionId) {
assignments.push({ memberId: orgMembershipId, attributeOptionId: optionId });
}
}
}
}
if (assignments.length > 0) {
await prisma.attributeToUser.createMany({ data: assignments });
const orgMembershipIds = Array.from(new Set(assignments.map((a) => a.memberId)));
const created = await prisma.attributeToUser.findMany({
where: { memberId: { in: orgMembershipIds } },
select: { id: true },
});
createdResources.attributeToUsers.push(...created.map((a) => a.id));
}
};
async function createAttributesScenario(params: {
attributes: {
id: string;
name: string;
type: AttributeType;
slug: string;
options: { id: string; value: string; slug: string; isGroup?: boolean; contains?: string[] }[];
}[];
teamMembersWithAttributeOptionValuePerAttribute: {
userId?: number;
attributes: Record<string, string | string[]>;
}[];
}) {
const { attributes, teamMembersWithAttributeOptionValuePerAttribute } = params;
const createdAttributes = await Promise.all(
attributes.map((attr) =>
createTestAttribute({
orgId: testFixtures.org.id,
...attr,
})
)
);
const optionValueToId = new Map<string, string>();
for (const attr of attributes) {
for (const opt of attr.options) {
optionValueToId.set(`${attr.id}:${opt.value}`, opt.id);
}
}
const numMembers = teamMembersWithAttributeOptionValuePerAttribute.length;
const timestamp = Date.now();
const userDataList = Array.from({ length: numMembers }, (_, i) => ({
email: `test-user-${timestamp}-${i}@example.com`,
username: `test-user-${timestamp}-${i}`,
}));
const orderedUsers = await createTestUsers(userDataList);
const { userIdToOrgMembershipId } = await createTestMembershipsForUsers({
userIds: orderedUsers.map((u) => u.id),
orgId: testFixtures.org.id,
teamId: testFixtures.team.id,
});
await createTestAttributeAssignments({
orderedUserIds: orderedUsers.map((u) => u.id),
userIdToOrgMembershipId,
membersAttributes: teamMembersWithAttributeOptionValuePerAttribute.map((m) => m.attributes),
optionValueToId,
});
const createdUsers = orderedUsers.map((user) => ({
userId: user.id,
orgMembershipId: userIdToOrgMembershipId.get(user.id)!,
}));
return { createdAttributes, createdUsers };
}
async function createHugeAttributesOfTypeSingleSelect(params: {
numAttributes: number;
numOptionsPerAttribute: number;
numTeamMembers: number;
numAttributesUsedPerTeamMember: number;
}) {
const { numAttributes, numOptionsPerAttribute, numTeamMembers, numAttributesUsedPerTeamMember } = params;
if (numAttributesUsedPerTeamMember > numAttributes) {
throw new Error("numAttributesUsedPerTeamMember cannot be greater than numAttributes");
}
const timestamp = Date.now();
const attributes = Array.from({ length: numAttributes }, (_, i) => ({
id: `perf-attr-${timestamp}-${i + 1}`,
name: `Attribute ${i + 1}`,
type: AttributeType.SINGLE_SELECT,
slug: `perf-attribute-${timestamp}-${i + 1}`,
options: Array.from({ length: numOptionsPerAttribute }, (_, j) => ({
id: `perf-opt-${timestamp}-${i}-${j + 1}`,
value: `Option ${j + 1}`,
slug: `perf-option-${timestamp}-${i}-${j + 1}`,
})),
}));
const assignedAttributeOptionIdForEachMember = 1;
const teamMembersWithAttributeOptionValuePerAttribute = Array.from({ length: numTeamMembers }, (_, _i) => ({
attributes: Object.fromEntries(
Array.from({ length: numAttributesUsedPerTeamMember }, (_, j) => [
attributes[j].id,
attributes[j].options[assignedAttributeOptionIdForEachMember].value,
])
),
}));
await createAttributesScenario({
attributes,
teamMembersWithAttributeOptionValuePerAttribute,
});
return {
attributes,
teamMembersWithAttributeOptionValuePerAttribute,
};
}
const cleanupCreatedResources = async () => {
if (createdResources.attributeToUsers.length > 0) {
await prisma.attributeToUser.deleteMany({
where: { id: { in: createdResources.attributeToUsers } },
});
createdResources.attributeToUsers = [];
}
if (createdResources.attributes.length > 0) {
await prisma.attribute.deleteMany({
where: { id: { in: createdResources.attributes } },
});
createdResources.attributes = [];
}
if (createdResources.memberships.length > 0) {
await prisma.membership.deleteMany({
where: { id: { in: createdResources.memberships } },
});
createdResources.memberships = [];
}
if (createdResources.users.length > 0) {
await prisma.user.deleteMany({
where: { id: { in: createdResources.users } },
});
createdResources.users = [];
}
};
function buildQueryValue({
rules,
}: {
rules: {
raqbFieldId: string;
value: string | number | string[] | [string[]];
operator: string;
valueSrc: NonNullable<BaseWidget["valueSrc"]>[];
valueType: string[];
}[];
}) {
const queryValue = {
id: "query-id-1",
type: "group",
children1: rules.reduce((acc, rule, index) => {
acc[`rule-${index + 1}`] = {
type: "rule",
properties: {
field: rule.raqbFieldId,
value: rule.value,
operator: rule.operator,
valueSrc: rule.valueSrc,
valueType: rule.valueType,
},
};
return acc;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}, {} as any),
};
return queryValue;
}
function buildSelectTypeFieldQueryValue({
rules,
}: {
rules: {
raqbFieldId: string;
value: string | number | string[] | [string[]];
operator: string;
valueType?: string[];
}[];
}) {
return buildQueryValue({
rules: rules.map((rule) => ({
raqbFieldId: rule.raqbFieldId,
value: rule.value,
operator: rule.operator,
valueSrc: ["value"],
valueType: rule.valueType ?? ["select"],
})),
}) as AttributesQueryValue;
}
function buildRoute({
id,
action,
queryValue,
attributesQueryValue,
}: {
id: string;
action: {
type: RouteActionType;
value: string;
};
queryValue: FormFieldsQueryValue;
attributesQueryValue?: AttributesQueryValue;
}) {
return {
id,
action,
queryValue,
attributesQueryValue,
};
}
function buildDefaultCustomPageRoute({
id,
attributesQueryValue,
}: {
id: string;
attributesQueryValue?: AttributesQueryValue;
}) {
return buildRoute({
id,
action: { type: RouteActionType.CustomPageMessage, value: "test" },
queryValue: { type: "group" } as unknown as FormFieldsQueryValue,
attributesQueryValue,
});
}
async function buildScenarioWhereMainAttributeLogicFails() {
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
const Option2OfAttribute1 = { id: "opt2", value: "Option 2", slug: "option-2" };
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1],
};
await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const failingAttributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option2OfAttribute1.id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const matchingAttributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option1OfAttribute1.id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
return { failingAttributesQueryValue, matchingAttributesQueryValue };
}
describe("findTeamMembersMatchingAttributeLogic", () => {
beforeAll(async () => {
const org = await createTestOrganization();
const team = await createTestTeam({ parentId: org.id });
testFixtures = { org, team };
});
afterAll(async () => {
await prisma.team.deleteMany({
where: { id: { in: [testFixtures.team.id, testFixtures.org.id] } },
});
});
afterEach(async () => {
await cleanupCreatedResources();
});
it("should return null if the route does not have an attributesQueryValue set", async () => {
await createAttributesScenario({
attributes: [],
teamMembersWithAttributeOptionValuePerAttribute: [],
});
const { teamMembersMatchingAttributeLogic, troubleshooter } = await findTeamMembersMatchingAttributeLogic(
{
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue: { type: "group" } as unknown as AttributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
},
{
enableTroubleshooter: true,
}
);
expect(teamMembersMatchingAttributeLogic).toBeNull();
expect(troubleshooter).toEqual(
expect.objectContaining({
type: TroubleshooterCase.MATCHES_ALL_MEMBERS_BECAUSE_OF_EMPTY_QUERY_VALUE,
})
);
});
describe("with a SINGLE_SELECT attribute", () => {
it("when a static option is selected, troubleshooter should be null by default", async () => {
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option1OfAttribute1.id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result, troubleshooter } =
await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([
{
userId: createdUsers[0].userId,
result: RaqbLogicResult.MATCH,
},
]);
expect(troubleshooter).toBeUndefined();
});
describe("with `Value of field`", () => {
it("when 'Value of Field' option is selected and ", async () => {
const Option1OfAttribute1HumanReadableValue = "Option 1";
const Option1OfField1HumanReadableValue = Option1OfAttribute1HumanReadableValue;
const Field1Id = "field-1";
const Option1OfAttribute1 = {
id: "attr-1-opt-1",
value: Option1OfAttribute1HumanReadableValue,
slug: "option-1",
};
const Option1OfField1 = {
id: "field-1-opt-1",
label: Option1OfField1HumanReadableValue,
};
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [`{field:${Field1Id}}`],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [
{
id: Field1Id,
type: RoutingFormFieldType.SINGLE_SELECT,
label: "Field 1",
options: [Option1OfField1],
},
],
response: {
[Field1Id]: {
value: Option1OfAttribute1HumanReadableValue,
label: Option1OfAttribute1HumanReadableValue,
},
},
},
attributesQueryValue: attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([
{
userId: createdUsers[0].userId,
result: RaqbLogicResult.MATCH,
},
]);
});
});
it("when 'Any in'(select_any_in) option is selected", async () => {
const Option1OfAttribute1HumanReadableValue = "Option 1";
const Option1OfAttribute1 = {
id: "attr-1-opt-1",
value: Option1OfAttribute1HumanReadableValue,
slug: "option-1",
};
const Option2OfAttribute1 = {
id: "attr-1-opt-2",
value: "Option 2",
slug: "option-2",
};
const Option3OfAttribute1 = {
id: "attr-1-opt-3",
value: "Option 3",
slug: "option-3",
};
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
operator: "select_any_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([
{
userId: createdUsers[0].userId,
result: RaqbLogicResult.MATCH,
},
]);
});
});
describe("with a MULTI_SELECT attribute", () => {
it("when 'Any in'(multiselect_some_in) option is selected and just one option is used in attribute for the user", async () => {
const Option1OfAttribute1HumanReadableValue = "Option 1";
const Option1OfAttribute1 = {
id: "attr-1-opt-1",
value: Option1OfAttribute1HumanReadableValue,
slug: "option-1",
};
const Option2OfAttribute1 = {
id: "attr-1-opt-2",
value: "Option 2",
slug: "option-2",
};
const Option3OfAttribute1 = {
id: "attr-1-opt-3",
value: "Option 3",
slug: "option-3",
};
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.MULTI_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
operator: "multiselect_some_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([
{
userId: createdUsers[0].userId,
result: RaqbLogicResult.MATCH,
},
]);
});
it("when 'Any in'(multiselect_some_in) option is selected and more than one option is used in attribute for the user", async () => {
const Option1OfAttribute1HumanReadableValue = "Option 1";
const Option1OfAttribute1 = {
id: "attr-1-opt-1",
value: Option1OfAttribute1HumanReadableValue,
slug: "option-1",
};
const Option2OfAttribute1 = {
id: "attr-1-opt-2",
value: "Option 2",
slug: "option-2",
};
const Option3OfAttribute1 = {
id: "attr-1-opt-3",
value: "Option 3",
slug: "option-3",
};
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.MULTI_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{
attributes: { [Attribute1.id]: [Option2OfAttribute1.value, Option1OfAttribute1.value] },
},
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
operator: "multiselect_some_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([
{
userId: createdUsers[0].userId,
result: RaqbLogicResult.MATCH,
},
]);
});
describe("with `Value of field`", () => {
it("when 'Any in'(multiselect_some_in) option is selected and more than one option is used in attribute and more than one option is selected in the field", async () => {
const Field1Id = "location-field";
const DelhiOption = {
id: "delhi-opt",
label: "Delhi",
};
const HaryanaOption = {
id: "haryana-opt",
label: "Haryana",
};
const MumbaiOption = {
id: "mumbai-opt",
label: "Mumbai",
};
const HeadquartersAttribute = {
id: "headquarters-attr",
name: "Headquarters",
type: AttributeType.MULTI_SELECT,
slug: "headquarters",
options: [
{ id: "delhi-hq", value: "Delhi", slug: "delhi" },
{ id: "haryana-hq", value: "Haryana", slug: "haryana" },
{ id: "mumbai-hq", value: "Mumbai", slug: "mumbai" },
{ id: "chennai-hq", value: "Chennai", slug: "chennai" },
],
};
const { createdUsers } = await createAttributesScenario({
attributes: [HeadquartersAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [HeadquartersAttribute.id]: ["Delhi", "Haryana"] } },
{ attributes: { [HeadquartersAttribute.id]: ["Mumbai"] } },
{ attributes: { [HeadquartersAttribute.id]: ["Chennai"] } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: HeadquartersAttribute.id,
value: [[`{field:${Field1Id}}`]],
operator: "multiselect_some_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result1 } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [
{
id: Field1Id,
type: RoutingFormFieldType.MULTI_SELECT,
label: "Location",
options: [DelhiOption, HaryanaOption, MumbaiOption],
},
],
response: {
[Field1Id]: { label: "Location", value: [DelhiOption.id, MumbaiOption.id] },
},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result1).toEqual(
expect.arrayContaining([
{ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result1).not.toContainEqual({ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH });
const { teamMembersMatchingAttributeLogic: result2 } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [
{
id: Field1Id,
type: RoutingFormFieldType.MULTI_SELECT,
label: "Location",
options: [DelhiOption, HaryanaOption, MumbaiOption, { id: "chennai-opt", label: "Chennai" }],
},
],
response: {
[Field1Id]: { label: "Location", value: ["chennai-opt"] },
},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result2).toEqual([{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH }]);
});
});
it("should handle field template mixed with regular values in multiselect array", async () => {
const LocationFieldId = "location-field";
const FixedValueId = "fixed-location-id";
const LocationAttribute = {
id: "location-attr",
name: "Service Locations",
type: AttributeType.MULTI_SELECT,
slug: "service-locations",
options: [
{ id: "delhi-loc", value: "Delhi", slug: "delhi" },
{ id: "mumbai-loc", value: "Mumbai", slug: "mumbai" },
{ id: FixedValueId, value: "Chennai", slug: "chennai" },
],
};
const { createdUsers } = await createAttributesScenario({
attributes: [LocationAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [LocationAttribute.id]: ["Delhi", "Chennai"] } },
{ attributes: { [LocationAttribute.id]: ["Mumbai"] } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: LocationAttribute.id,
value: [[`{field:${LocationFieldId}}`, "Chennai"]],
operator: "multiselect_some_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [
{
id: LocationFieldId,
type: RoutingFormFieldType.MULTI_SELECT,
label: "Preferred Location",
options: [
{ id: "delhi-opt", label: "Delhi" },
{ id: "mumbai-opt", label: "Mumbai" },
],
},
],
response: {
[LocationFieldId]: { value: ["Mumbai"], label: "Mumbai" },
},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
])
);
});
});
describe("Fallback", () => {
it("should return null when main attribute logic fails and no fallback is defined", async () => {
const { failingAttributesQueryValue } = await buildScenarioWhereMainAttributeLogicFails();
const {
teamMembersMatchingAttributeLogic: result,
checkedFallback,
troubleshooter,
} = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue: failingAttributesQueryValue,
fallbackAttributesQueryValue: null,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(null);
expect(checkedFallback).toEqual(true);
expect(troubleshooter).toBeUndefined();
});
it("should return matching members when main attribute logic fails and but fallback matches", async () => {
const { failingAttributesQueryValue, matchingAttributesQueryValue } =
await buildScenarioWhereMainAttributeLogicFails();
const {
teamMembersMatchingAttributeLogic: result,
checkedFallback,
troubleshooter,
} = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue: failingAttributesQueryValue,
fallbackAttributesQueryValue: matchingAttributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(checkedFallback).toEqual(true);
expect(result).toHaveLength(1);
expect(result![0].result).toBe(RaqbLogicResult.MATCH);
expect(troubleshooter).toBeUndefined();
});
it("should return 0 matching members when main attribute logic and fallback attribute logic fail", async () => {
const { failingAttributesQueryValue } = await buildScenarioWhereMainAttributeLogicFails();
const {
teamMembersMatchingAttributeLogic: result,
checkedFallback,
troubleshooter,
} = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue: failingAttributesQueryValue,
fallbackAttributesQueryValue: failingAttributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(checkedFallback).toEqual(true);
expect(troubleshooter).toBeUndefined();
expect(result).toEqual([]);
});
});
describe("Error handling", () => {
it("should return null for attributes with unsupported types (TEXT with select operators)", async () => {
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.TEXT,
slug: "attribute-1",
options: [Option1OfAttribute1],
};
await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{
attributes: { [Attribute1.id]: Option1OfAttribute1.value },
},
],
});
const { teamMembersMatchingAttributeLogic } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue: buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option1OfAttribute1.id],
operator: "select_equals",
},
],
}),
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(teamMembersMatchingAttributeLogic).toBeNull();
});
it("should return warnings in preview and live mode", async () => {
const Option1OfAttribute1HumanReadableValue = "Option 1";
const Option1OfAttribute1 = {
id: "attr-1-opt-1",
value: Option1OfAttribute1HumanReadableValue,
slug: "option-1",
};
const Option2OfAttribute1 = {
id: "attr-1-opt-2",
value: "Option 2",
slug: "option-2",
};
const Option3OfAttribute1 = {
id: "attr-1-opt-3",
value: "Option 3",
slug: "option-3",
};
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
};
await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [["NON_EXISTING_OPTION_1", "NON_EXISTING_OPTION_2"]],
operator: "select_any_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
async function runInMode({ mode }: { mode: "preview" | "live" }) {
const result = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
isPreview: mode === "preview" ? true : false,
fallbackAttributesQueryValue: null,
});
return result;
}
await (async function liveMode() {
const result = await runInMode({ mode: "live" });
expect(result.teamMembersMatchingAttributeLogic).toEqual(null);
expect(result.mainAttributeLogicBuildingWarnings).toEqual([
"Value NON_EXISTING_OPTION_1 is not in list of values",
]);
})();
await (async function previewMode() {
const result = await runInMode({ mode: "preview" });
expect(result.teamMembersMatchingAttributeLogic).toEqual(null);
expect(result.mainAttributeLogicBuildingWarnings).toEqual([
"Value NON_EXISTING_OPTION_1 is not in list of values",
]);
})();
});
it("should not throw error if children1 is empty", async () => {
const Option1OfAttribute1HumanReadableValue = "Option 1";
const Option1OfAttribute1 = {
id: "attr-1-opt-1",
value: Option1OfAttribute1HumanReadableValue,
slug: "option-1",
};
const Option2OfAttribute1 = {
id: "attr-1-opt-2",
value: "Option 2",
slug: "option-2",
};
const Option3OfAttribute1 = {
id: "attr-1-opt-3",
value: "Option 3",
slug: "option-3",
};
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
};
await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildQueryValue({
rules: [],
}) as AttributesQueryValue;
expect(attributesQueryValue.children1).toEqual({});
async function runInMode({ mode }: { mode: "preview" | "live" }) {
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
isPreview: mode === "preview" ? true : false,
});
return result;
}
await (async function liveMode() {
const result = await runInMode({ mode: "live" });
expect(result).toEqual(null);
})();
await (async function previewMode() {
const result = await runInMode({ mode: "preview" });
expect(result).toEqual(null);
})();
});
});
describe("Performance testing", () => {
describe("20 attributes, 400 team members", async () => {
it("should return matching team members with a SINGLE_SELECT attribute when 'all in' option is selected", async () => {
const { attributes } = await createHugeAttributesOfTypeSingleSelect({
numAttributes: 20,
numOptionsPerAttribute: 30,
numTeamMembers: 400,
numAttributesUsedPerTeamMember: 10,
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: attributes[0].id,
value: [attributes[0].options[1].id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result, timeTaken } =
await findTeamMembersMatchingAttributeLogic(
{
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
},
{
concurrency: 1,
enablePerf: true,
}
);
expect(result).toHaveLength(400);
expect(result).toEqual(
expect.arrayContaining([expect.objectContaining({ result: RaqbLogicResult.MATCH })])
);
if (!timeTaken) {
throw new Error("Looks like performance testing is not enabled");
}
const totalTimeTaken = Object.values(timeTaken).reduce((sum, time) => (sum ?? 0) + (time ?? 0), 0);
console.log("Total time taken", totalTimeTaken, {
timeTaken,
});
expect(totalTimeTaken).toBeLessThan(5000);
}, 60000);
});
});
describe("Troubleshooter", () => {
it("troubleshooter data should be there when enableTroubleshooter is true", async () => {
const Option1OfAttribute1HumanReadableValue = "Option 1";
const Option1OfAttribute1 = {
id: "attr-1-opt-1",
value: Option1OfAttribute1HumanReadableValue,
slug: "option-1",
};
const Option2OfAttribute1 = {
id: "attr-1-opt-2",
value: "Option 2",
slug: "option-2",
};
const Option3OfAttribute1 = {
id: "attr-1-opt-3",
value: "Option 3",
slug: "option-3",
};
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
operator: "select_any_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result, troubleshooter } =
await findTeamMembersMatchingAttributeLogic(
{
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
},
{
enableTroubleshooter: true,
}
);
expect(result).toEqual([
{
userId: createdUsers[0].userId,
result: RaqbLogicResult.MATCH,
},
]);
expect(troubleshooter).not.toBeUndefined();
});
});
it("should handle non-existent option IDs gracefully", async () => {
const LocationAttribute = {
id: "location-attr",
name: "Location",
type: AttributeType.SINGLE_SELECT,
slug: "location",
options: [{ id: "ny-opt", value: "New York", slug: "new-york" }],
};
await createAttributesScenario({
attributes: [LocationAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [LocationAttribute.id]: "New York" } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: LocationAttribute.id,
value: ["non-existent-id"],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([]);
});
describe("routingFormTrace integration", () => {
it("should call attributeLogicEvaluated when routingFormTrace is provided and main logic matches", async () => {
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option1OfAttribute1.id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const mockRoutingFormTrace = {
attributeLogicEvaluated: vi.fn(),
attributeFallbackUsed: vi.fn(),
routeMatched: vi.fn(),
fallbackRouteUsed: vi.fn(),
};
await findTeamMembersMatchingAttributeLogic(
{
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
routeName: "Test Route",
routeIsFallback: false,
},
{
routingFormTraceService: mockRoutingFormTrace as never,
}
);
expect(mockRoutingFormTrace.attributeLogicEvaluated).toHaveBeenCalledWith(
expect.objectContaining({
routeName: "Test Route",
routeIsFallback: false,
checkedFallback: false,
})
);
});
it("should call attributeLogicEvaluated with checkedFallback=true when fallback is used", async () => {
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
const Option2OfAttribute1 = { id: "opt2", value: "Option 2", slug: "option-2" };
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const failingAttributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option2OfAttribute1.id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const matchingFallbackQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option1OfAttribute1.id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const mockRoutingFormTrace = {
attributeLogicEvaluated: vi.fn(),
attributeFallbackUsed: vi.fn(),
routeMatched: vi.fn(),
fallbackRouteUsed: vi.fn(),
};
await findTeamMembersMatchingAttributeLogic(
{
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue: failingAttributesQueryValue,
fallbackAttributesQueryValue: matchingFallbackQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
routeName: "Test Route",
routeIsFallback: false,
},
{
routingFormTraceService: mockRoutingFormTrace as never,
}
);
expect(mockRoutingFormTrace.attributeLogicEvaluated).toHaveBeenCalledWith(
expect.objectContaining({
routeName: "Test Route",
routeIsFallback: false,
checkedFallback: true,
})
);
});
it("should not call attributeLogicEvaluated when routingFormTrace is not provided", async () => {
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: AttributeType.SINGLE_SELECT,
slug: "attribute-1",
options: [Option1OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option1OfAttribute1.id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const mockRoutingFormTrace = {
attributeLogicEvaluated: vi.fn(),
attributeFallbackUsed: vi.fn(),
routeMatched: vi.fn(),
fallbackRouteUsed: vi.fn(),
};
await findTeamMembersMatchingAttributeLogic(
{
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
},
{}
);
expect(mockRoutingFormTrace.attributeLogicEvaluated).not.toHaveBeenCalled();
});
it("should include attributeRoutingDetails in trace when attributes are used", async () => {
const Option1OfAttribute1 = { id: "opt1", value: "Enterprise", slug: "enterprise" };
const Attribute1 = {
id: "attr1",
name: "Company Size",
type: AttributeType.SINGLE_SELECT,
slug: "company-size",
options: [Option1OfAttribute1],
};
const { createdUsers } = await createAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [Option1OfAttribute1.id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const mockRoutingFormTrace = {
attributeLogicEvaluated: vi.fn(),
attributeFallbackUsed: vi.fn(),
routeMatched: vi.fn(),
fallbackRouteUsed: vi.fn(),
};
await findTeamMembersMatchingAttributeLogic(
{
dynamicFieldValueOperands: {
fields: [],
response: {},
},
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
routeName: "Enterprise Route",
routeIsFallback: false,
},
{
routingFormTraceService: mockRoutingFormTrace as never,
}
);
expect(mockRoutingFormTrace.attributeLogicEvaluated).toHaveBeenCalledWith(
expect.objectContaining({
attributeRoutingDetails: expect.arrayContaining([
expect.objectContaining({
attributeName: "Company Size",
}),
]),
})
);
});
});
describe("negation operators with users who have no attribute assignment", () => {
const DepartmentAttribute = {
id: "dept-attr",
name: "Department",
type: AttributeType.SINGLE_SELECT,
slug: "department",
options: [
{ id: "dept-sales", value: "Sales", slug: "sales" },
{ id: "dept-eng", value: "Engineering", slug: "engineering" },
{ id: "dept-marketing", value: "Marketing", slug: "marketing" },
],
};
const LocationsAttribute = {
id: "locs-attr",
name: "Locations",
type: AttributeType.MULTI_SELECT,
slug: "locations",
options: [
{ id: "loc-nyc", value: "NYC", slug: "nyc" },
{ id: "loc-la", value: "LA", slug: "la" },
{ id: "loc-chicago", value: "Chicago", slug: "chicago" },
],
};
describe("select_not_equals", () => {
it("should match users without the attribute (undefined != 'Sales' is true)", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [DepartmentAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [DepartmentAttribute.id]: "Sales" } },
{ attributes: { [DepartmentAttribute.id]: "Engineering" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: DepartmentAttribute.id,
value: ["dept-sales"],
operator: "select_not_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
it("should match users who have a different attribute but not the queried one (undefined != 'Sales' is true)", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [DepartmentAttribute, LocationsAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [DepartmentAttribute.id]: "Sales" } },
{ attributes: { [DepartmentAttribute.id]: "Engineering" } },
// User has Location but NOT Department - should still match Department != 'Sales'
{ attributes: { [LocationsAttribute.id]: ["Chicago"] } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: DepartmentAttribute.id,
value: ["dept-sales"],
operator: "select_not_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
// User 1 (Engineering) and User 2 (has Location but no Department) should match
// because Engineering != Sales and undefined != Sales are both true
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
});
describe("select_not_any_in", () => {
it("should match users without the attribute (undefined not in ['Sales', 'Marketing'] is true)", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [DepartmentAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [DepartmentAttribute.id]: "Sales" } },
{ attributes: { [DepartmentAttribute.id]: "Engineering" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: DepartmentAttribute.id,
value: [["dept-sales", "dept-marketing"]],
operator: "select_not_any_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
});
describe("multiselect_not_equals (!all)", () => {
it("should match users without the attribute (not all of undefined in values is true)", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [LocationsAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [LocationsAttribute.id]: ["NYC", "LA"] } },
{ attributes: { [LocationsAttribute.id]: ["Chicago"] } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: LocationsAttribute.id,
value: [["loc-nyc", "loc-la"]],
operator: "multiselect_not_equals",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
});
describe("multiselect_not_some_in (!some)", () => {
it("should match users without the attribute (not some of undefined in values is true)", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [LocationsAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [LocationsAttribute.id]: ["NYC"] } },
{ attributes: { [LocationsAttribute.id]: ["Chicago"] } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: LocationsAttribute.id,
value: [["loc-nyc", "loc-la"]],
operator: "multiselect_not_some_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
});
describe("positive operators should NOT match users without the attribute", () => {
it("select_equals should not match users without the attribute", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [DepartmentAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [DepartmentAttribute.id]: "Sales" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: DepartmentAttribute.id,
value: ["dept-sales"],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([{ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH }]);
});
it("select_any_in should not match users without the attribute", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [DepartmentAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [DepartmentAttribute.id]: "Sales" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: DepartmentAttribute.id,
value: [["dept-sales", "dept-marketing"]],
operator: "select_any_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([{ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH }]);
});
it("multiselect_some_in should not match users without the attribute", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [LocationsAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [LocationsAttribute.id]: ["NYC"] } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: LocationsAttribute.id,
value: [["loc-nyc", "loc-la"]],
operator: "multiselect_some_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([{ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH }]);
});
it("multiselect_equals (all) should not match users without the attribute", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [LocationsAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [LocationsAttribute.id]: ["NYC", "LA"] } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: LocationsAttribute.id,
value: [["loc-nyc", "loc-la"]],
operator: "multiselect_equals",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual([{ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH }]);
});
});
});
describe("negation operators with TEXT and NUMBER attribute types", () => {
describe("TEXT not_equal", () => {
it("should match users without the attribute (undefined != 'sales manager' is true)", async () => {
const JobTitleAttribute = {
id: "job-title-attr",
name: "Job Title",
type: AttributeType.TEXT,
slug: "job-title",
options: [
{ id: "job-sales-mgr", value: "Sales Manager", slug: "sales-manager" },
{ id: "job-engineer", value: "Engineer", slug: "engineer" },
],
};
const { createdUsers } = await createAttributesScenario({
attributes: [JobTitleAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [JobTitleAttribute.id]: "Sales Manager" } },
{ attributes: { [JobTitleAttribute.id]: "Engineer" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildQueryValue({
rules: [
{
raqbFieldId: JobTitleAttribute.id,
value: ["sales manager"],
operator: "not_equal",
valueSrc: ["value"],
valueType: ["text"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
});
describe("TEXT not_like", () => {
it("should match users without the attribute (undefined not contains 'engineer' is true)", async () => {
const JobTitleAttribute = {
id: "job-title-attr-2",
name: "Job Title",
type: AttributeType.TEXT,
slug: "job-title-2",
options: [
{ id: "job-sr-eng", value: "Senior Engineer", slug: "senior-engineer" },
{ id: "job-designer", value: "Designer", slug: "designer" },
],
};
const { createdUsers } = await createAttributesScenario({
attributes: [JobTitleAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [JobTitleAttribute.id]: "Senior Engineer" } },
{ attributes: { [JobTitleAttribute.id]: "Designer" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildQueryValue({
rules: [
{
raqbFieldId: JobTitleAttribute.id,
value: ["engineer"],
operator: "not_like",
valueSrc: ["value"],
valueType: ["text"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
});
describe("NUMBER not_equal", () => {
it("should match users without the attribute (undefined != '5' is true)", async () => {
const ExperienceAttribute = {
id: "exp-attr",
name: "Experience Years",
type: AttributeType.NUMBER,
slug: "experience-years",
options: [
{ id: "exp-5", value: "5", slug: "5" },
{ id: "exp-10", value: "10", slug: "10" },
],
};
const { createdUsers } = await createAttributesScenario({
attributes: [ExperienceAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{ attributes: { [ExperienceAttribute.id]: "5" } },
{ attributes: { [ExperienceAttribute.id]: "10" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildQueryValue({
rules: [
{
raqbFieldId: ExperienceAttribute.id,
value: ["5"],
operator: "not_equal",
valueSrc: ["value"],
valueType: ["number"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
});
});
describe("compound rules with users missing some attributes", () => {
const DepartmentAttribute = {
id: "dept-attr-2",
name: "Department",
type: AttributeType.SINGLE_SELECT,
slug: "department-2",
options: [
{ id: "dept-sales-2", value: "Sales", slug: "sales" },
{ id: "dept-eng-2", value: "Engineering", slug: "engineering" },
],
};
const LocationAttribute = {
id: "loc-attr-2",
name: "Location",
type: AttributeType.SINGLE_SELECT,
slug: "location-2",
options: [
{ id: "loc-nyc-2", value: "NYC", slug: "nyc" },
{ id: "loc-la-2", value: "LA", slug: "la" },
],
};
it("positive AND negation: should match user with one attribute but missing the negated one", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [DepartmentAttribute, LocationAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{
attributes: { [DepartmentAttribute.id]: "Sales", [LocationAttribute.id]: "NYC" },
},
{ attributes: { [DepartmentAttribute.id]: "Sales" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: DepartmentAttribute.id,
value: ["dept-sales-2"],
operator: "select_equals",
},
{
raqbFieldId: LocationAttribute.id,
value: ["loc-nyc-2"],
operator: "select_not_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
// User 0: Dept=Sales (== matches) AND Location=NYC (!= fails) -> NO MATCH
// User 1: Dept=Sales (== matches) AND Location=undefined (!= matches) -> MATCH
// User 2: no Dept (== fails) -> NO MATCH (AND short-circuits)
expect(result).toEqual([{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH }]);
});
it("multiple negation rules: should match users missing different attributes", async () => {
const { createdUsers } = await createAttributesScenario({
attributes: [DepartmentAttribute, LocationAttribute],
teamMembersWithAttributeOptionValuePerAttribute: [
{
attributes: { [DepartmentAttribute.id]: "Sales", [LocationAttribute.id]: "NYC" },
},
{ attributes: { [DepartmentAttribute.id]: "Engineering" } },
{ attributes: { [LocationAttribute.id]: "LA" } },
{ attributes: {} },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: DepartmentAttribute.id,
value: ["dept-sales-2"],
operator: "select_not_equals",
},
{
raqbFieldId: LocationAttribute.id,
value: ["loc-nyc-2"],
operator: "select_not_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
dynamicFieldValueOperands: { fields: [], response: {} },
attributesQueryValue,
teamId: testFixtures.team.id,
orgId: testFixtures.org.id,
});
// User 0: Dept=Sales (!= fails) -> NO MATCH
// User 1: Dept=Engineering (!= matches) AND Location=undefined (!= matches) -> MATCH
// User 2: Dept=undefined (!= matches) AND Location=LA (!= matches) -> MATCH
// User 3: both undefined (both != match) -> MATCH
expect(result).toEqual(
expect.arrayContaining([
{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH },
{ userId: createdUsers[3].userId, result: RaqbLogicResult.MATCH },
])
);
expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH });
});
});
});