feat: Support an array response for a field when used as Value of Field (#22740)
* Passing tests and fixed * self review addressed adn more tests
This commit is contained in:
@@ -47,6 +47,7 @@ function mockAttributesScenario({
|
||||
...attribute,
|
||||
options: attribute.options.map((option) => ({
|
||||
...option,
|
||||
attributeId: attribute.id,
|
||||
...commonOptionsProps,
|
||||
})),
|
||||
})),
|
||||
@@ -289,35 +290,176 @@ describe("findTeamMembersMatchingAttributeLogic", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should return matching team members with a SINGLE_SELECT attribute when a static option is selected and 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: "SINGLE_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1],
|
||||
};
|
||||
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: "SINGLE_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
||||
],
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, 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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
result: RaqbLogicResult.MATCH,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(troubleshooter).toBeUndefined();
|
||||
});
|
||||
|
||||
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
||||
rules: [
|
||||
{
|
||||
raqbFieldId: Attribute1.id,
|
||||
value: [Option1OfAttribute1.id],
|
||||
operator: "select_equals",
|
||||
},
|
||||
],
|
||||
}) as AttributesQueryValue;
|
||||
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 { teamMembersMatchingAttributeLogic: result, troubleshooter } =
|
||||
await findTeamMembersMatchingAttributeLogic({
|
||||
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: "SINGLE_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, 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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
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: "SINGLE_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, 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: {},
|
||||
@@ -327,285 +469,328 @@ describe("findTeamMembersMatchingAttributeLogic", () => {
|
||||
orgId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
result: RaqbLogicResult.MATCH,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(troubleshooter).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return matching team members with a SINGLE_SELECT attribute when 'Value of Field' option is selected", 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: "SINGLE_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, 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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
result: RaqbLogicResult.MATCH,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return matching team members with a SINGLE_SELECT attribute 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: "SINGLE_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, 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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
result: RaqbLogicResult.MATCH,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return matching team members with a MULTI_SELECT attribute 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: "MULTI_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
// user 1 has only one option selected for the attribute
|
||||
{ userId: 1, 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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
result: RaqbLogicResult.MATCH,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return matching team members with a MULTI_SELECT attribute 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: "MULTI_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
// user 1 has two options selected for the attribute
|
||||
attributes: { [Attribute1.id]: [Option2OfAttribute1.value, Option1OfAttribute1.value] },
|
||||
result: RaqbLogicResult.MATCH,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
||||
rules: [
|
||||
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: "MULTI_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
// user 1 has only one option selected for the attribute
|
||||
{ userId: 1, 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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
raqbFieldId: Attribute1.id,
|
||||
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
|
||||
operator: "multiselect_some_in",
|
||||
valueType: ["multiselect"],
|
||||
userId: 1,
|
||||
result: RaqbLogicResult.MATCH,
|
||||
},
|
||||
],
|
||||
}) as AttributesQueryValue;
|
||||
|
||||
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
||||
dynamicFieldValueOperands: {
|
||||
fields: [],
|
||||
response: {},
|
||||
},
|
||||
attributesQueryValue,
|
||||
teamId: 1,
|
||||
orgId,
|
||||
]);
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
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: "MULTI_SELECT" as const,
|
||||
slug: "attribute-1",
|
||||
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [Attribute1],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{
|
||||
userId: 1,
|
||||
// user 1 has two options selected for the attribute
|
||||
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: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
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: "MULTI_SELECT" as const,
|
||||
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" },
|
||||
],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [HeadquartersAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
// User 1: headquarters in Delhi and Haryana
|
||||
{ userId: 1, attributes: { [HeadquartersAttribute.id]: ["Delhi", "Haryana"] } },
|
||||
// User 2: headquarters only in Mumbai
|
||||
{ userId: 2, attributes: { [HeadquartersAttribute.id]: ["Mumbai"] } },
|
||||
// User 3: headquarters in Chennai
|
||||
{ userId: 3, attributes: { [HeadquartersAttribute.id]: ["Chennai"] } },
|
||||
],
|
||||
});
|
||||
|
||||
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
||||
rules: [
|
||||
{
|
||||
raqbFieldId: HeadquartersAttribute.id,
|
||||
value: [[`{field:${Field1Id}}`]],
|
||||
operator: "multiselect_some_in",
|
||||
valueType: ["multiselect"],
|
||||
},
|
||||
],
|
||||
}) as AttributesQueryValue;
|
||||
|
||||
// Test case 1: Booker selects Delhi and Mumbai
|
||||
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] }, // Booker selected Delhi and Mumbai
|
||||
},
|
||||
},
|
||||
attributesQueryValue,
|
||||
teamId: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// Should match users 1 and 2 (Delhi matches user 1, Mumbai matches user 2)
|
||||
expect(result1).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ userId: 1, result: RaqbLogicResult.MATCH },
|
||||
{ userId: 2, result: RaqbLogicResult.MATCH },
|
||||
])
|
||||
);
|
||||
expect(result1).not.toContainEqual({ userId: 3, result: RaqbLogicResult.MATCH });
|
||||
|
||||
// Test case 2: Booker selects only Chennai
|
||||
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"] }, // Booker selected only Chennai
|
||||
},
|
||||
},
|
||||
attributesQueryValue,
|
||||
teamId: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// Should match only user 3
|
||||
expect(result2).toEqual([{ userId: 3, 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: "MULTI_SELECT" as const,
|
||||
slug: "service-locations",
|
||||
options: [
|
||||
{ id: "delhi-loc", value: "Delhi", slug: "delhi" },
|
||||
{ id: "mumbai-loc", value: "Mumbai", slug: "mumbai" },
|
||||
{ id: FixedValueId, value: "Chennai", slug: "chennai" },
|
||||
],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [LocationAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
// User 1: services Delhi and Chennai
|
||||
{ userId: 1, attributes: { [LocationAttribute.id]: ["Delhi", "Chennai"] } },
|
||||
// User 2: services only Mumbai
|
||||
{ userId: 2, attributes: { [LocationAttribute.id]: ["Mumbai"] } },
|
||||
],
|
||||
});
|
||||
|
||||
// This simulates a complex rule: Location must include field value OR Chennai (fixed)
|
||||
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
||||
rules: [
|
||||
{
|
||||
raqbFieldId: LocationAttribute.id,
|
||||
value: [[`{field:${LocationFieldId}}`, "Chennai"]], // Mixed: field template + fixed value
|
||||
operator: "multiselect_some_in",
|
||||
valueType: ["multiselect"],
|
||||
},
|
||||
],
|
||||
}) as AttributesQueryValue;
|
||||
|
||||
// When booker selects Mumbai
|
||||
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" }, // Booker selected Mumbai
|
||||
},
|
||||
},
|
||||
attributesQueryValue,
|
||||
teamId: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// Should match both users:
|
||||
// User 1: matches because they service Chennai (the fixed value)
|
||||
// User 2: matches because they service Mumbai (the field value)
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ userId: 1, result: RaqbLogicResult.MATCH },
|
||||
{ userId: 2, result: RaqbLogicResult.MATCH },
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Fallback", () => {
|
||||
@@ -1025,4 +1210,44 @@ describe("findTeamMembersMatchingAttributeLogic", () => {
|
||||
expect(troubleshooter).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle non-existent option IDs gracefully", async () => {
|
||||
const LocationAttribute = {
|
||||
id: "location-attr",
|
||||
name: "Location",
|
||||
type: "SINGLE_SELECT" as const,
|
||||
slug: "location",
|
||||
options: [{ id: "ny-opt", value: "New York", slug: "new-york" }],
|
||||
};
|
||||
|
||||
mockAttributesScenario({
|
||||
attributes: [LocationAttribute],
|
||||
teamMembersWithAttributeOptionValuePerAttribute: [
|
||||
{ userId: 1, attributes: { [LocationAttribute.id]: "New York" } },
|
||||
],
|
||||
});
|
||||
|
||||
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
||||
rules: [
|
||||
{
|
||||
raqbFieldId: LocationAttribute.id,
|
||||
value: ["non-existent-id"], // Non-existent option ID
|
||||
operator: "select_equals",
|
||||
},
|
||||
],
|
||||
}) as AttributesQueryValue;
|
||||
|
||||
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
||||
dynamicFieldValueOperands: {
|
||||
fields: [],
|
||||
response: {},
|
||||
},
|
||||
attributesQueryValue,
|
||||
teamId: 1,
|
||||
orgId,
|
||||
});
|
||||
|
||||
// Should not match anyone as the option ID doesn't exist
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+10
-118
@@ -4,9 +4,7 @@ import { Utils as QbUtils } from "react-awesome-query-builder";
|
||||
|
||||
import { getQueryBuilderConfigForAttributes } from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig";
|
||||
import type { LocalRoute } from "@calcom/app-store/routing-forms/types/types";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { dynamicFieldValueOperands, dynamicFieldValueOperandsResponse } from "@calcom/lib/raqb/types";
|
||||
import type { AttributesQueryValue } from "@calcom/lib/raqb/types";
|
||||
import type { dynamicFieldValueOperands } from "@calcom/lib/raqb/types";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type {
|
||||
AttributeOptionValueWithType,
|
||||
@@ -15,61 +13,13 @@ import type {
|
||||
} from "@calcom/lib/service/attribute/server/getAttributes";
|
||||
import { AttributeType } from "@calcom/prisma/enums";
|
||||
|
||||
const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/lib/raqbUtils"] });
|
||||
|
||||
function getFieldResponseValueAsLabel({
|
||||
field,
|
||||
fieldResponseValue,
|
||||
}: {
|
||||
fieldResponseValue: dynamicFieldValueOperandsResponse[keyof dynamicFieldValueOperandsResponse]["value"];
|
||||
field: {
|
||||
type: string;
|
||||
options?: {
|
||||
id: string | null;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
}) {
|
||||
if (!field.options) {
|
||||
return fieldResponseValue;
|
||||
}
|
||||
|
||||
const valueArray = fieldResponseValue instanceof Array ? fieldResponseValue : [fieldResponseValue];
|
||||
|
||||
const chosenOptions = valueArray.map((idOrLabel) => {
|
||||
const foundOptionById = field.options?.find((option) => {
|
||||
return option.id === idOrLabel;
|
||||
});
|
||||
if (foundOptionById) {
|
||||
return {
|
||||
label: foundOptionById.label,
|
||||
id: foundOptionById.id,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: idOrLabel.toString(),
|
||||
id: null,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return chosenOptions.map((option) => option.label);
|
||||
}
|
||||
import { resolveQueryValue } from "./resolveQueryValue";
|
||||
import { caseInsensitive } from "./utils";
|
||||
|
||||
function ensureArray(value: string | string[]) {
|
||||
return typeof value === "string" ? [value] : value;
|
||||
}
|
||||
|
||||
function caseInsensitive<T extends string | string[]>(
|
||||
stringOrStringArray: T
|
||||
): T extends string[] ? string[] : string {
|
||||
return (
|
||||
stringOrStringArray instanceof Array
|
||||
? stringOrStringArray.map((string) => string.toLowerCase())
|
||||
: stringOrStringArray.toLowerCase()
|
||||
) as T extends string[] ? string[] : string;
|
||||
}
|
||||
|
||||
export const raqbQueryValueUtils = {
|
||||
isQueryValueARuleGroup: function isQueryValueARuleGroup(queryValue: JsonTree): queryValue is JsonGroup {
|
||||
return queryValue.type === "group";
|
||||
@@ -144,61 +94,6 @@ export const buildStateFromQueryValue = ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace attribute option Ids with the attribute option label(compatible to be matched with form field value)
|
||||
*/
|
||||
const replaceAttributeOptionIdsWithOptionLabel = ({
|
||||
queryValueString,
|
||||
attributes,
|
||||
}: {
|
||||
queryValueString: string;
|
||||
attributes: Attribute[];
|
||||
}) => {
|
||||
const allAttributesOptions = attributes.map((attribute) => attribute.options).flat();
|
||||
// Because all attribute option Ids are unique, we can reliably identify them along any number of attribute options of different attributes
|
||||
allAttributesOptions.forEach((attributeOption) => {
|
||||
const attributeOptionId = attributeOption.id;
|
||||
queryValueString = queryValueString.replace(
|
||||
new RegExp(`${attributeOptionId}`, "g"),
|
||||
caseInsensitive(attributeOption.value)
|
||||
);
|
||||
});
|
||||
return queryValueString;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace {field:<fieldId>} with the field label(compatible to be matched with attribute value)
|
||||
*/
|
||||
const replaceFieldTemplateVariableWithOptionLabel = ({
|
||||
queryValueString,
|
||||
dynamicFieldValueOperands,
|
||||
}: {
|
||||
queryValueString: string;
|
||||
dynamicFieldValueOperands?: dynamicFieldValueOperands;
|
||||
}) => {
|
||||
if (!dynamicFieldValueOperands) {
|
||||
return queryValueString;
|
||||
}
|
||||
const { fields, response } = dynamicFieldValueOperands;
|
||||
return queryValueString.replace(/{field:([\w-]+)}/g, (match, fieldId: string) => {
|
||||
const field = fields?.find((f) => f.id === fieldId);
|
||||
if (!field) {
|
||||
moduleLogger.debug("field not found", safeStringify({ fieldId }));
|
||||
return match;
|
||||
}
|
||||
const fieldResponseValue = response[fieldId]?.value;
|
||||
if (!fieldResponseValue) {
|
||||
return match;
|
||||
}
|
||||
const responseValueAsLabel = getFieldResponseValueAsLabel({ field, fieldResponseValue });
|
||||
moduleLogger.debug("matchingOptionLabel", safeStringify({ responseValueAsLabel, response, fieldId }));
|
||||
if (responseValueAsLabel instanceof Array && responseValueAsLabel.length > 1) {
|
||||
throw new Error("Array value not supported with 'Value of field'");
|
||||
}
|
||||
return responseValueAsLabel ? caseInsensitive(responseValueAsLabel.toString()) : match;
|
||||
});
|
||||
};
|
||||
|
||||
export function getValueOfAttributeOption(
|
||||
attributeOptions:
|
||||
| Pick<AttributeOptionValue, "isGroup" | "contains" | "value">
|
||||
@@ -262,17 +157,13 @@ function getAttributesQueryValue({
|
||||
return null;
|
||||
}
|
||||
|
||||
const attributesQueryValueCompatibleForMatchingWithFormField: AttributesQueryValue = JSON.parse(
|
||||
replaceFieldTemplateVariableWithOptionLabel({
|
||||
queryValueString: replaceAttributeOptionIdsWithOptionLabel({
|
||||
queryValueString: JSON.stringify(attributesQueryValue),
|
||||
attributes,
|
||||
}),
|
||||
dynamicFieldValueOperands,
|
||||
})
|
||||
);
|
||||
const resolvedQueryValue = resolveQueryValue({
|
||||
queryValue: attributesQueryValue,
|
||||
attributes,
|
||||
dynamicFieldValueOperands,
|
||||
});
|
||||
|
||||
return attributesQueryValueCompatibleForMatchingWithFormField;
|
||||
return resolvedQueryValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,4 +220,5 @@ export const acrossQueryValueCompatiblity = {
|
||||
getAttributesQueryBuilderConfigHavingListofLabels,
|
||||
getAttributesQueryValue,
|
||||
getAttributesData,
|
||||
resolveQueryValue,
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,267 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { dynamicFieldValueOperands, dynamicFieldValueOperandsResponse } from "@calcom/lib/raqb/types";
|
||||
import type { AttributesQueryValue } from "@calcom/lib/raqb/types";
|
||||
import type { Attribute } from "@calcom/lib/service/attribute/server/getAttributes";
|
||||
|
||||
import { caseInsensitive } from "./utils";
|
||||
|
||||
// Type for JSON values that can be in the query
|
||||
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
||||
type MinimalField = {
|
||||
type: string;
|
||||
options?: {
|
||||
id: string | null;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type FieldResponseValue = dynamicFieldValueOperandsResponse[keyof dynamicFieldValueOperandsResponse]["value"];
|
||||
|
||||
const moduleLogger = logger.getSubLogger({ prefix: ["raqb/resolveQueryValue"] });
|
||||
|
||||
/**
|
||||
* Replace "Attribute Options' Ids" with "Attribute Options' Labels". It makes the query value compatible to be matched with 'Value of Field' operand
|
||||
*/
|
||||
const replaceAttributeOptionIdsWithOptionLabel = ({
|
||||
queryValue,
|
||||
attributes,
|
||||
}: {
|
||||
queryValue: AttributesQueryValue;
|
||||
attributes: Attribute[];
|
||||
}) => {
|
||||
const queryValueString = JSON.stringify(queryValue);
|
||||
let queryValueWithLabels = queryValueString;
|
||||
const allAttributesOptions = attributes.map((attribute) => attribute.options).flat();
|
||||
// Because all attribute option Ids are unique, we can reliably identify them along any number of attribute options of different attributes
|
||||
allAttributesOptions.forEach((attributeOption) => {
|
||||
const attributeOptionId = attributeOption.id;
|
||||
queryValueWithLabels = queryValueWithLabels.replace(
|
||||
new RegExp(`${attributeOptionId}`, "g"),
|
||||
caseInsensitive(attributeOption.value)
|
||||
);
|
||||
});
|
||||
return JSON.parse(queryValueWithLabels) as AttributesQueryValue;
|
||||
};
|
||||
|
||||
function getFieldResponseValueAsLabel({
|
||||
field,
|
||||
fieldResponseValue,
|
||||
}: {
|
||||
fieldResponseValue: FieldResponseValue;
|
||||
field: MinimalField;
|
||||
}) {
|
||||
const nonNumberFieldResponseValue =
|
||||
typeof fieldResponseValue === "number" ? fieldResponseValue.toString() : fieldResponseValue;
|
||||
|
||||
if (!field.options) {
|
||||
return nonNumberFieldResponseValue;
|
||||
}
|
||||
|
||||
if (nonNumberFieldResponseValue instanceof Array) {
|
||||
return nonNumberFieldResponseValue.map(transformValue);
|
||||
}
|
||||
|
||||
return transformValue(nonNumberFieldResponseValue);
|
||||
|
||||
function transformValue(idOrLabel: string) {
|
||||
const foundOptionById = field.options?.find((option) => {
|
||||
return option.id === idOrLabel;
|
||||
});
|
||||
if (foundOptionById) {
|
||||
return foundOptionById.label;
|
||||
} else {
|
||||
return idOrLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves query values by:
|
||||
* 1. Converting attribute option IDs to their lowercase labels
|
||||
* 2. Replacing field template placeholders (e.g., "{field:location}") with actual field values
|
||||
*
|
||||
* Important: When a field template resolves to an array value, the array items are flattened
|
||||
* into the parent array. For example:
|
||||
* - Input: [["{{field:location}}", "Delhi"]] where location = ["New York", "Amsterdam"]
|
||||
* - Output: [["new york", "amsterdam", "Delhi"]] (flattened into single array)
|
||||
* @param queryValue - JSON string containing RAQB query with field template placeholders like {field:fieldId}
|
||||
* @param dynamicFieldValueOperands - Optional object containing fields metadata and user's form field responses
|
||||
* @param dynamicFieldValueOperands.fields - Array of field definitions with id, type, label, etc.
|
||||
* @param dynamicFieldValueOperands.response - Object mapping field IDs to their response values
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* resolveQueryValue({
|
||||
* queryValue: {
|
||||
* "id": "9b8ba888-0123-4456-b89a-b1984020d699",
|
||||
* "type": "group",
|
||||
* "children1": {
|
||||
* "a8a89bba-89ab-4cde-b012-31984020ebaa": {
|
||||
* "type": "rule",
|
||||
* "id": "a8a89bba-89ab-4cde-b012-31984020ebaa",
|
||||
* "properties": {
|
||||
* "field": "88be6104-d350-411d-aa0f-092b2deda257",
|
||||
* "operator": "multiselect_some_in",
|
||||
* "value": [
|
||||
* [
|
||||
* "{field:0bf77a89-2bd0-4df7-9648-758014ba3189}", // 'Value of field' is stored like this and we call it a 'field template'
|
||||
* "899c846d-7c02-43dc-9057-c3f8c118d41f" // A regular attribute option id
|
||||
* ]
|
||||
* ],
|
||||
* "valueSrc": [
|
||||
* "value"
|
||||
* ],
|
||||
* "valueError": [
|
||||
* null
|
||||
* ],
|
||||
* "valueType": [
|
||||
* "multiselect"
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* },
|
||||
* dynamicFieldValueOperands: {
|
||||
* fields: [
|
||||
* { id: "location", type: "MULTI_SELECT", label: "Location" }
|
||||
* ],
|
||||
* response: {
|
||||
* location: { value: ["Delhi", "Mumbai"] }
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* // Returns: '{"value": [["delhi", "mumbai"]], "operator": "multiselect_some_in"}'
|
||||
* ```
|
||||
*/
|
||||
export const resolveQueryValue = ({
|
||||
queryValue,
|
||||
dynamicFieldValueOperands,
|
||||
attributes,
|
||||
}: {
|
||||
queryValue: AttributesQueryValue;
|
||||
dynamicFieldValueOperands?: dynamicFieldValueOperands;
|
||||
attributes: Attribute[];
|
||||
}) => {
|
||||
const queryValueWithLabels = replaceAttributeOptionIdsWithOptionLabel({
|
||||
queryValue,
|
||||
attributes,
|
||||
});
|
||||
|
||||
if (!dynamicFieldValueOperands) {
|
||||
return queryValueWithLabels;
|
||||
}
|
||||
const { fields, response } = dynamicFieldValueOperands;
|
||||
|
||||
const isFieldTemplate = (str: string): boolean => {
|
||||
const trimmedStr = str.trim();
|
||||
return trimmedStr.startsWith("{field:") && trimmedStr.endsWith("}");
|
||||
};
|
||||
|
||||
// Extract field ID from template string
|
||||
const extractFieldId = (template: string): string | null => {
|
||||
const trimmedTemplate = template.trim();
|
||||
if (!isFieldTemplate(trimmedTemplate)) return null;
|
||||
// Remove {field: and } to get the ID
|
||||
return trimmedTemplate.slice(7, -1);
|
||||
};
|
||||
|
||||
// Resolve a field ID to its actual value
|
||||
const resolveFieldId = (fieldId: string): string | string[] | null => {
|
||||
const field = fields.find((f) => f.id === fieldId);
|
||||
if (!field || !response[fieldId]?.value) {
|
||||
// Maybe invalid fieldId
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldResponseValue = response[fieldId].value;
|
||||
return getFieldResponseValueAsLabel({ field, fieldResponseValue });
|
||||
};
|
||||
|
||||
// Process a string value that might be a field template and thus could be an array
|
||||
const processStringValue = (value: string): string | string[] => {
|
||||
const fieldId = extractFieldId(value);
|
||||
if (!fieldId) {
|
||||
// Regular non-field template string
|
||||
return value;
|
||||
}
|
||||
|
||||
const resolvedValue = resolveFieldId(fieldId);
|
||||
if (!resolvedValue) {
|
||||
// We keep the field template look-alike as it is
|
||||
return value;
|
||||
}
|
||||
|
||||
return caseInsensitive(resolvedValue);
|
||||
};
|
||||
|
||||
// Process an array that might contain field templates. We keep it generic instead of typing it as QueryValue to handle any property where the queryValue can have a field template
|
||||
// Note: When a field template resolves to an array, its items are flattened into the parent array
|
||||
const processArray = (arr: JsonValue[]): JsonValue[] => {
|
||||
const hasFieldTemplate = arr.some((item) => typeof item === "string" && isFieldTemplate(item));
|
||||
|
||||
// Short circuit if there are no field templates
|
||||
if (!hasFieldTemplate) {
|
||||
return arr.map((item) => processAnyValue(item));
|
||||
}
|
||||
|
||||
const result: JsonValue[] = [];
|
||||
for (const item of arr) {
|
||||
if (typeof item === "string" && isFieldTemplate(item)) {
|
||||
const processed = processStringValue(item);
|
||||
if (Array.isArray(processed)) {
|
||||
// Flattening behavior: Array values from field templates are spread into parent array
|
||||
// Example: ["{field:location}", "Delhi"] where location = ["New York", "Amsterdam"]
|
||||
// Result: ["New York", "Amsterdam", "Delhi"] (items flattened)
|
||||
// This is intentional to merge field values at the same array level
|
||||
result.push(...processed);
|
||||
} else {
|
||||
// [{field:location}, Delhi] -> becomes [New York, Delhi]. New York was the response for the field `location`
|
||||
result.push(processed);
|
||||
}
|
||||
} else {
|
||||
result.push(processAnyValue(item));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Process any value recursively
|
||||
const processAnyValue = (value: JsonValue): JsonValue => {
|
||||
if (value instanceof Array) {
|
||||
return processArray(value);
|
||||
}
|
||||
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const result: { [key: string]: JsonValue } = {};
|
||||
for (const key in value) {
|
||||
result[key] = processAnyValue(value[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const processedValue = processStringValue(value);
|
||||
return processedValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
try {
|
||||
// Even though it recursively processes the queryValue, the object(see example in `resolveQueryValue` fn signature) isn't that complex and thus the performance is not an issue
|
||||
const processed = processAnyValue(queryValueWithLabels) as AttributesQueryValue;
|
||||
return processed;
|
||||
} catch (e) {
|
||||
moduleLogger.error("Failed to resolve field templates", {
|
||||
error: e,
|
||||
queryValue: JSON.stringify(queryValueWithLabels),
|
||||
fieldsCount: fields?.length || 0,
|
||||
responseKeys: Object.keys(response || {}),
|
||||
dynamicFieldValueOperandsProvided: !!dynamicFieldValueOperands,
|
||||
});
|
||||
throw new Error(
|
||||
`Failed to resolve field templates in query value: ${e instanceof Error ? e.message : String(e)}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export function caseInsensitive<T extends string | string[]>(
|
||||
stringOrStringArray: T
|
||||
): T extends string[] ? string[] : string {
|
||||
return (
|
||||
stringOrStringArray instanceof Array
|
||||
? stringOrStringArray.map((string) => string.toLowerCase())
|
||||
: stringOrStringArray.toLowerCase()
|
||||
) as T extends string[] ? string[] : string;
|
||||
}
|
||||
@@ -83,6 +83,40 @@ const raqbChildren1Schema = z
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @example
|
||||
* ```json
|
||||
* {
|
||||
"id": "9b8ba888-0123-4456-b89a-b1984020d699",
|
||||
"type": "group",
|
||||
"children1": {
|
||||
"a8a89bba-89ab-4cde-b012-31984020ebaa": {
|
||||
"type": "rule",
|
||||
"properties": {
|
||||
"field": "88be6104-d350-411d-aa0f-092b2deda257",
|
||||
"operator": "multiselect_some_in",
|
||||
"value": [
|
||||
[
|
||||
"{field:0bf77a89-2bd0-4df7-9648-758014ba3189}",
|
||||
"899c846d-7c02-43dc-9057-c3f8c118d41f"
|
||||
]
|
||||
],
|
||||
"valueSrc": [
|
||||
"value"
|
||||
],
|
||||
"valueError": [
|
||||
null
|
||||
],
|
||||
"valueType": [
|
||||
"multiselect"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
* ```
|
||||
*/
|
||||
export const raqbQueryValueSchema = z.union([
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
|
||||
Reference in New Issue
Block a user