fix: Exclusion attribute filter (#27669)
* Fix exclusion filter - include all team members * Fix display when members aren't saved in the DB * Update tests * Undo change to member switch
This commit is contained in:
@@ -734,7 +734,7 @@ describe("getAttributes", () => {
|
||||
|
||||
it("should handle empty attributes gracefully", async () => {
|
||||
const team = await createMockTeam({ orgId });
|
||||
await createMockUserHavingMembershipWithBothTeamAndOrg({
|
||||
const { user } = await createMockUserHavingMembershipWithBothTeamAndOrg({
|
||||
orgId,
|
||||
teamId: team.id,
|
||||
});
|
||||
@@ -752,8 +752,13 @@ describe("getAttributes", () => {
|
||||
const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } =
|
||||
await getAttributesAssignmentData({ teamId: team.id, orgId });
|
||||
|
||||
// No assignments since there are no options to assign
|
||||
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(0);
|
||||
// Team member should still be included (with empty attributes) even though there are no assignments.
|
||||
// This is important for "not any in" filters where members without the attribute should match.
|
||||
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1);
|
||||
expect(attributesAssignedToTeamMembersWithOptions[0]).toEqual({
|
||||
userId: user.id,
|
||||
attributes: {},
|
||||
});
|
||||
expect(attributesOfTheOrg).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -985,6 +990,94 @@ describe("getAttributes", () => {
|
||||
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1);
|
||||
expect(Object.keys(attributesAssignedToTeamMembersWithOptions[0].attributes)).toEqual(["attr1"]);
|
||||
});
|
||||
|
||||
it("should include team members without any assignment for queried attributes (important for 'not any in' filters)", async () => {
|
||||
const team = await createMockTeam({ orgId });
|
||||
|
||||
// User 1 has Department assignment
|
||||
const { user: user1, orgMembership: orgMembership1 } =
|
||||
await createMockUserHavingMembershipWithBothTeamAndOrg({
|
||||
orgId,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
// User 2 only has Location assignment (no Department)
|
||||
const user2 = await prismock.user.create({
|
||||
data: { name: "User 2", email: "user2@test.com" },
|
||||
});
|
||||
const orgMembership2 = await prismock.membership.create({
|
||||
data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: orgId, userId: user2.id },
|
||||
});
|
||||
await prismock.membership.create({
|
||||
data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: team.id, userId: user2.id },
|
||||
});
|
||||
|
||||
// User 3 has no attribute assignments at all
|
||||
const user3 = await prismock.user.create({
|
||||
data: { name: "User 3", email: "user3@test.com" },
|
||||
});
|
||||
await prismock.membership.create({
|
||||
data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: orgId, userId: user3.id },
|
||||
});
|
||||
await prismock.membership.create({
|
||||
data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: team.id, userId: user3.id },
|
||||
});
|
||||
|
||||
await createMockAttribute({
|
||||
orgId,
|
||||
id: "dept-attr",
|
||||
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: [] },
|
||||
],
|
||||
});
|
||||
|
||||
await createMockAttribute({
|
||||
orgId,
|
||||
id: "loc-attr",
|
||||
name: "Location",
|
||||
slug: "location",
|
||||
type: AttributeType.SINGLE_SELECT,
|
||||
options: [{ id: "loc-nyc", value: "NYC", slug: "nyc", isGroup: false, contains: [] }],
|
||||
});
|
||||
|
||||
// User 1: Department = Engineering
|
||||
await createMockAttributeAssignment({
|
||||
orgMembershipId: orgMembership1.id,
|
||||
attributeOptionId: "dept-eng",
|
||||
});
|
||||
|
||||
// User 2: Location = NYC (no Department)
|
||||
await createMockAttributeAssignment({
|
||||
orgMembershipId: orgMembership2.id,
|
||||
attributeOptionId: "loc-nyc",
|
||||
});
|
||||
|
||||
// Query only for Department attribute (simulating "Department not any in [Sales]" filter)
|
||||
const { attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({
|
||||
teamId: team.id,
|
||||
orgId,
|
||||
attributeIds: ["dept-attr"],
|
||||
});
|
||||
|
||||
// All 3 team members should be included, even those without Department assignment
|
||||
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(3);
|
||||
|
||||
const user1Data = attributesAssignedToTeamMembersWithOptions.find((m) => m.userId === user1.id);
|
||||
const user2Data = attributesAssignedToTeamMembersWithOptions.find((m) => m.userId === user2.id);
|
||||
const user3Data = attributesAssignedToTeamMembersWithOptions.find((m) => m.userId === user3.id);
|
||||
|
||||
// User 1 has Department assignment
|
||||
expect(user1Data?.attributes["dept-attr"]).toBeDefined();
|
||||
expect(user1Data?.attributes["dept-attr"].attributeOption).toMatchObject({ value: "Engineering" });
|
||||
|
||||
// User 2 and User 3 should be included with empty attributes (for "not any in" evaluation)
|
||||
expect(user2Data?.attributes).toEqual({});
|
||||
expect(user3Data?.attributes).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -129,70 +129,81 @@ async function _findMembershipsForBothOrgAndTeam({
|
||||
function _prepareAssignmentData({
|
||||
assignmentsForTheTeam,
|
||||
lookupMaps,
|
||||
allTeamMemberIds,
|
||||
}: {
|
||||
assignmentsForTheTeam: AssignmentForTheTeam[];
|
||||
lookupMaps: AttributeLookupMaps;
|
||||
/** All team member user IDs - ensures members without assignments are included */
|
||||
allTeamMemberIds: UserId[];
|
||||
}) {
|
||||
const { optionIdToOption, attributeIdToOptions } = lookupMaps;
|
||||
|
||||
const teamMembersThatHaveOptionAssigned = assignmentsForTheTeam.reduce(
|
||||
(acc, attributeToUser) => {
|
||||
const userId = attributeToUser.userId;
|
||||
const attributeOption = attributeToUser.attributeOption;
|
||||
const attribute = attributeToUser.attribute;
|
||||
// Group assignments by userId for O(1) lookup
|
||||
const assignmentsByUserId = new Map<UserId, AssignmentForTheTeam[]>();
|
||||
for (const assignment of assignmentsForTheTeam) {
|
||||
const existing = assignmentsByUserId.get(assignment.userId);
|
||||
if (existing) {
|
||||
existing.push(assignment);
|
||||
} else {
|
||||
assignmentsByUserId.set(assignment.userId, [assignment]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!acc[userId]) {
|
||||
acc[userId] = { userId, attributes: {} };
|
||||
}
|
||||
// Iterate over all team members once, applying their assignments if any.
|
||||
// This ensures members without assignments are still included (important for
|
||||
// "not any in" filters where members without the attribute should match).
|
||||
const result: {
|
||||
userId: UserId;
|
||||
attributes: Record<AttributeId, AttributeOptionValueWithType>;
|
||||
}[] = [];
|
||||
|
||||
const attributes = acc[userId].attributes;
|
||||
const currentAttributeOptionValue =
|
||||
attributes[attribute.id]?.attributeOption;
|
||||
const newAttributeOptionValue = {
|
||||
isGroup: attributeOption.isGroup,
|
||||
value: attributeOption.value,
|
||||
contains: tranformContains({
|
||||
contains: attributeOption.contains,
|
||||
attribute,
|
||||
}),
|
||||
};
|
||||
for (const userId of allTeamMemberIds) {
|
||||
const memberData: {
|
||||
userId: UserId;
|
||||
attributes: Record<AttributeId, AttributeOptionValueWithType>;
|
||||
} = { userId, attributes: {} };
|
||||
|
||||
if (currentAttributeOptionValue instanceof Array) {
|
||||
attributes[attribute.id].attributeOption = [
|
||||
...currentAttributeOptionValue,
|
||||
newAttributeOptionValue,
|
||||
];
|
||||
} else if (currentAttributeOptionValue) {
|
||||
attributes[attribute.id].attributeOption = [
|
||||
currentAttributeOptionValue,
|
||||
{
|
||||
isGroup: attributeOption.isGroup,
|
||||
value: attributeOption.value,
|
||||
contains: tranformContains({
|
||||
contains: attributeOption.contains,
|
||||
attribute,
|
||||
}),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// Set the first value
|
||||
attributes[attribute.id] = {
|
||||
type: attribute.type,
|
||||
attributeOption: newAttributeOptionValue,
|
||||
const userAssignments = assignmentsByUserId.get(userId);
|
||||
if (userAssignments) {
|
||||
for (const attributeToUser of userAssignments) {
|
||||
const attributeOption = attributeToUser.attributeOption;
|
||||
const attribute = attributeToUser.attribute;
|
||||
const attributes = memberData.attributes;
|
||||
|
||||
const currentAttributeOptionValue = attributes[attribute.id]?.attributeOption;
|
||||
const newAttributeOptionValue = {
|
||||
isGroup: attributeOption.isGroup,
|
||||
value: attributeOption.value,
|
||||
contains: tranformContains({
|
||||
contains: attributeOption.contains,
|
||||
attribute,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
UserId,
|
||||
{
|
||||
userId: UserId;
|
||||
attributes: Record<AttributeId, AttributeOptionValueWithType>;
|
||||
}
|
||||
>
|
||||
);
|
||||
|
||||
return Object.values(teamMembersThatHaveOptionAssigned);
|
||||
if (currentAttributeOptionValue instanceof Array) {
|
||||
attributes[attribute.id].attributeOption = [
|
||||
...currentAttributeOptionValue,
|
||||
newAttributeOptionValue,
|
||||
];
|
||||
} else if (currentAttributeOptionValue) {
|
||||
attributes[attribute.id].attributeOption = [
|
||||
currentAttributeOptionValue,
|
||||
newAttributeOptionValue,
|
||||
];
|
||||
} else {
|
||||
// Set the first value
|
||||
attributes[attribute.id] = {
|
||||
type: attribute.type,
|
||||
attributeOption: newAttributeOptionValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(memberData);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
/**
|
||||
* Transforms ["optionId1", "optionId2"] to [{
|
||||
@@ -508,9 +519,24 @@ export async function getAttributesAssignmentData({
|
||||
lookupMaps,
|
||||
});
|
||||
|
||||
const allTeamMemberIds = Array.from(orgMembershipToUserIdForTeamMembers.values());
|
||||
|
||||
logger.debug("getAttributesAssignmentData", {
|
||||
teamId,
|
||||
orgId,
|
||||
attributeIds,
|
||||
allTeamMemberIdsCount: allTeamMemberIds.length,
|
||||
assignmentsForTheTeamCount: assignmentsForTheTeam.length,
|
||||
});
|
||||
|
||||
const attributesAssignedToTeamMembersWithOptions = _prepareAssignmentData({
|
||||
assignmentsForTheTeam,
|
||||
lookupMaps,
|
||||
allTeamMemberIds,
|
||||
});
|
||||
|
||||
logger.debug("getAttributesAssignmentData result", {
|
||||
attributesAssignedToTeamMembersWithOptionsCount: attributesAssignedToTeamMembersWithOptions.length,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1505,4 +1505,330 @@ describe("findTeamMembersMatchingAttributeLogic", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("negation operators with users who have no attribute assignment", () => {
|
||||
// These tests verify that users without an attribute assignment are correctly
|
||||
// included in the evaluation when using negation operators. This is important
|
||||
// because users without the attribute should match "not" conditions.
|
||||
|
||||
const DepartmentAttribute = {
|
||||
id: "dept-attr",
|
||||
name: "Department",
|
||||
type: "SINGLE_SELECT" as const,
|
||||
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: "MULTI_SELECT" as const,
|
||||
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 () => {
|
||||
mockAttributesScenario({
|
||||
attributes: [DepartmentAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [DepartmentAttribute.id]: "Sales" } },
|
||||
{ userId: 2, attributes: { [DepartmentAttribute.id]: "Engineering" } },
|
||||
{ userId: 3, attributes: {} }, // No department assigned
|
||||
],
|
||||
});
|
||||
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// User 1 (Sales) should NOT match
|
||||
// User 2 (Engineering) should match
|
||||
// User 3 (no dept) should match (undefined != "Sales" is true)
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ userId: 2, result: RaqbLogicResult.MATCH },
|
||||
{ userId: 3, result: RaqbLogicResult.MATCH },
|
||||
])
|
||||
);
|
||||
expect(result).not.toContainEqual({ userId: 1, result: RaqbLogicResult.MATCH });
|
||||
});
|
||||
});
|
||||
|
||||
describe("select_not_any_in", () => {
|
||||
it("should match users without the attribute (undefined not in ['Sales', 'Marketing'] is true)", async () => {
|
||||
mockAttributesScenario({
|
||||
attributes: [DepartmentAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [DepartmentAttribute.id]: "Sales" } },
|
||||
{ userId: 2, attributes: { [DepartmentAttribute.id]: "Engineering" } },
|
||||
{ userId: 3, attributes: {} }, // No department assigned
|
||||
],
|
||||
});
|
||||
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// User 1 (Sales) should NOT match (Sales is in the excluded list)
|
||||
// User 2 (Engineering) should match (Engineering is not in excluded list)
|
||||
// User 3 (no dept) should match (undefined is not in excluded list)
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ userId: 2, result: RaqbLogicResult.MATCH },
|
||||
{ userId: 3, result: RaqbLogicResult.MATCH },
|
||||
])
|
||||
);
|
||||
expect(result).not.toContainEqual({ userId: 1, result: RaqbLogicResult.MATCH });
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiselect_not_equals (!all)", () => {
|
||||
it("should match users without the attribute (not all of undefined in values is true)", async () => {
|
||||
mockAttributesScenario({
|
||||
attributes: [LocationsAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [LocationsAttribute.id]: ["NYC", "LA"] } },
|
||||
{ userId: 2, attributes: { [LocationsAttribute.id]: ["Chicago"] } },
|
||||
{ userId: 3, attributes: {} }, // No locations assigned
|
||||
],
|
||||
});
|
||||
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// User 1 (NYC, LA) should NOT match (all their values are in the list)
|
||||
// User 2 (Chicago) should match (Chicago is not in the list)
|
||||
// User 3 (no locations) should match (undefined means not all values match)
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ userId: 2, result: RaqbLogicResult.MATCH },
|
||||
{ userId: 3, result: RaqbLogicResult.MATCH },
|
||||
])
|
||||
);
|
||||
expect(result).not.toContainEqual({ userId: 1, 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 () => {
|
||||
mockAttributesScenario({
|
||||
attributes: [LocationsAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [LocationsAttribute.id]: ["NYC"] } },
|
||||
{ userId: 2, attributes: { [LocationsAttribute.id]: ["Chicago"] } },
|
||||
{ userId: 3, attributes: {} }, // No locations assigned
|
||||
],
|
||||
});
|
||||
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// User 1 (NYC) should NOT match (NYC is in the list)
|
||||
// User 2 (Chicago) should match (Chicago is not in the list)
|
||||
// User 3 (no locations) should match (undefined means none of their values are in the list)
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ userId: 2, result: RaqbLogicResult.MATCH },
|
||||
{ userId: 3, result: RaqbLogicResult.MATCH },
|
||||
])
|
||||
);
|
||||
expect(result).not.toContainEqual({ userId: 1, result: RaqbLogicResult.MATCH });
|
||||
});
|
||||
});
|
||||
|
||||
describe("positive operators should NOT match users without the attribute", () => {
|
||||
it("select_equals should not match users without the attribute", async () => {
|
||||
mockAttributesScenario({
|
||||
attributes: [DepartmentAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [DepartmentAttribute.id]: "Sales" } },
|
||||
{ userId: 2, attributes: {} }, // No department assigned
|
||||
],
|
||||
});
|
||||
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// Only User 1 should match
|
||||
expect(result).toEqual([{ userId: 1, result: RaqbLogicResult.MATCH }]);
|
||||
});
|
||||
|
||||
it("select_any_in should not match users without the attribute", async () => {
|
||||
mockAttributesScenario({
|
||||
attributes: [DepartmentAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [DepartmentAttribute.id]: "Sales" } },
|
||||
{ userId: 2, attributes: {} }, // No department assigned
|
||||
],
|
||||
});
|
||||
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// Only User 1 should match
|
||||
expect(result).toEqual([{ userId: 1, result: RaqbLogicResult.MATCH }]);
|
||||
});
|
||||
|
||||
it("multiselect_some_in should not match users without the attribute", async () => {
|
||||
mockAttributesScenario({
|
||||
attributes: [LocationsAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [LocationsAttribute.id]: ["NYC"] } },
|
||||
{ userId: 2, attributes: {} }, // No locations assigned
|
||||
],
|
||||
});
|
||||
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// Only User 1 should match
|
||||
expect(result).toEqual([{ userId: 1, result: RaqbLogicResult.MATCH }]);
|
||||
});
|
||||
|
||||
it("multiselect_equals (all) should not match users without the attribute", async () => {
|
||||
mockAttributesScenario({
|
||||
attributes: [LocationsAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [LocationsAttribute.id]: ["NYC", "LA"] } },
|
||||
{ userId: 2, attributes: {} }, // No locations assigned
|
||||
],
|
||||
});
|
||||
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// Only User 1 should match
|
||||
expect(result).toEqual([{ userId: 1, result: RaqbLogicResult.MATCH }]);
|
||||
});
|
||||
});
|
||||
|
||||
// Note: is_null and is_not_null operators are listed in the multiselect operators array
|
||||
// but their definitions are commented out in BasicConfig.ts (lines 158-171).
|
||||
// These operators are not currently supported for attributes.
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user