api-documentation
api-documentation copied to clipboard
Bug | POST /contactfilter | "OR" Operators in "Expression" Parameter are Converted into "AND" Operators in the Mailjet Web Application When Creating Contact Filters via the API
What is the bug?
Given a user is making a POST
request to the /contactfilter
endpoint (Source Line | API Reference Docs | Developer Guide Docs),
and the user is using Mailjet JS to make the request,
and the request's Expression
body parameter contains OR
operators (e.g., "Expression": "(Contains(tags_subscribed_to, \"all\")) OR (Contains(tags_subscribed_to, \"math\")) OR (Contains(tags_subscribed_to, \"programming\"))"
) (Source Line),
when the user submits the request,
Expected result:
then the created contact filter as viewed in the Mailjet web application should OR
the conditions which were joined by the OR
operator in the request's Expression
parameter.
Actual result:
then the created contact filter as viewed in the Mailjet web application AND
s the conditions which were joined by the OR
operator in the request's Expression
parameter.
Note: The returned Segmentation.PostContactFilterResponse
's Expression
reflects what was sent exactly, as does retrieving the contact filter with a GET
request.
How did you produce it?
For some context, when I add a new post to my blog, I'm wanting to automatically notify those subscribers who are subscribed to the post's tags. My flow, then, is to either reuse a preexisting contact filter for segmenting my contact list based on the post's tags and my contacts' tags_subscribed_to
string
property (an example for one contact could be "math, programming, career"
), or in the case that a contact filter doesn't already exist for the given tags, to create a new one. Here is some code following that flow, using hard-coded values for the post tags:
// playground.ts
import { Client, LibraryResponse, Segmentation } from "node-mailjet";
const executeNewPostNotificationFlow = async (newPostId: string) => {
const postTags = ["math", "programming"];
const mailjet = new Client({
apiKey: getLocalVariableValue("MAILJET_API_KEY"),
apiSecret: getLocalVariableValue("MAILJET_SECRET_KEY"),
});
const filterId = await getContactFilterIdFromPostTags(postTags , mailjet);
console.log(filterId);
};
const getContactFilterIdFromPostTags = async (
postTags: string[],
mailjet: Client
): Promise<number> => {
const desiredFilterExpression = getFilterExpressionFromPostTags(postTags);
console.log(desiredFilterExpression);
const getFiltersRequest: Promise<LibraryResponse<Segmentation.GetContactFilterResponse>> = mailjet
.get("contactfilter", { version: "v3" })
.request();
return getFiltersRequest
.then((result) => {
const preExistingFilters = result.body.Data.filter((contactFilter) => {
return contactFilter.Expression === desiredFilterExpression;
});
if (preExistingFilters.length) {
return preExistingFilters[0].ID;
}
const createFilterRequest: Promise<LibraryResponse<Segmentation.PostContactFilterResponse>> =
mailjet.post("contactfilter", { version: "v3" }).request({
Description: "Will send only to contacts subscribed to the tags: " + postTags.toString(),
Expression: desiredFilterExpression,
Name: "Tags: " + postTags.toString(),
});
return createFilterRequest
.then((result) => {
return result.body.Data[0].ID;
})
.catch((err) => {
console.log(err);
return 0;
});
})
.catch((err) => {
console.log(err);
return 0;
});
};
const getFilterExpressionFromPostTags = (postTags: string[]) => {
return postTags.reduce((prev, cur) => {
return prev + " OR " + `(Contains(tags_subscribed_to, "${cur}"))`;
// eslint-disable-next-line quotes
}, '(Contains(tags_subscribed_to, "all"))');
};
// Keep the code below here
const main = async (): Promise<void> => {
await executeNewPostNotificationFlow("test-post");
};
const getLocalVariableValue = (variableName: string): string | undefined => {
...
};
main();
Running this code via Nodemon, I'll console-log (Contains(tags_subscribed_to, "all")) OR (Contains(tags_subscribed_to, "math")) OR (Contains(tags_subscribed_to, "programming"))
, and I'll successfully create (and on subsequent executions retrieve) the ID of the contact filter defined by the postTags
array. When I view the details of the created segment in the Mailjet web application, however, I'm seeing the following:
What else have you tried?
- I tried
AND
ing the conditions to see if that resulted inOR
s in the result (in case there was a mix-up or something), but that gave me the same outcome asOR
ing. - I tried adding an extra set of parentheses around each condition (just in case this line was pertinent — I think this would only apply in ambiguous distributive-property situations that arise when using both
OR
andAND
operators), but this also didn't help. - I manually created the desired segment with
OR
ed predicates, and retrieving it looked like:(Contains(tags_subscribed_to,"all") OR Contains(tags_subscribed_to,"math") OR Contains(tags_subscribed_to,"programming"))
. This caused me to think that maybe I just had to remove the spaces between the contact property and the value whose containment in the property we're checking, that is to try the following:
const getFilterExpressionFromPostTags = (postTags: string[]) => {
return postTags.reduce((prev, cur) => {
return prev + " OR " + `(Contains(tags_subscribed_to,"${cur}"))`;
// eslint-disable-next-line quotes
}, '(Contains(tags_subscribed_to,"all"))');
};
That also didn't work, though. Oddly, it caused the quotations that can be seen in the above screenshot to disappear. That is, the web application now displayed the created segment as follows (after deleting the other one):
I'm not sure what's going on here. I haven't tried actually using any of these segments; I'm just going strictly off what I see in the web application.
I didn't do a very good job at examining the difference between the Expression
returned from the manually-created segment and my own attempts. The correct filter-creating function looks like:
const getFilterExpressionFromPostTags = (postTags: string[]) => {
return postTags.reduce((prev, cur, curIdx, arr) => {
return (
prev +
" OR " +
`Contains(tags_subscribed_to,"${cur}")` +
(curIdx === arr.length - 1 ? ")" : "")
);
// eslint-disable-next-line quotes
}, '(Contains(tags_subscribed_to,"all")');
};
I needed to wrap the whole complex filter in a single set of parentheses rather than wrapping each predicate in its own set of parentheses.
Well actually, I want to reopen this, even though I got it working. I'm reopening because (a.) it seems to me that the docs don't make it clear that the entire Expression
must be wrapped in a single set of parentheses, and further (b.) because it seems that the POST
/contactfilter
endpoint doesn't behave predictably when its Expression
isn't formatted correctly. To explain what I mean:
(a.) I feel that the docs should indicate clearly that, if the user wants to OR
together 3 predicates, the following expression will not accomplish that:
"Expression": "(PredicateA) OR (PredicateB) OR (PredicateC)"
but that it should rather be formatted as follows:
"Expression": "(PredicateA OR PredicateB OR PredicateC)"
(b.) I feel that in the first case above, the POST
/contactfilter
endpoint should either return an error without creating a segment (a suggestion which maybe is outside of the scope of this repo), or at least that these docs should explain that such an Expression
will result in the predicates being AND
ed together, rather than OR
ed.
I also think the quotation-mark behavior with the Contains
operator in Expression
s could use some better understanding/documentation. For example, when there isn't a space between the given contact property and the given value in a Contains
operator:
mailjet.post("contactfilter", { version: "v3" }).request({
Description: "Will send only to contacts subscribed to the tags: math",
Expression: `Contains(tags_subscribed_to,"math")`,
Name: "Tags: math",
});
then the value doesn't have quotations around it in the web application:
However, when there is a space between the given contact property and the given value in a Contains
operator:
mailjet.post("contactfilter", { version: "v3" }).request({
Description: "Will send only to contacts subscribed to the tags: math",
Expression: `Contains(tags_subscribed_to, "math")`,
Name: "Tags: math",
});
then the value does have quotations around it in the web application:
Are these points that should be taken up in a PR? Or are they actually quite minor?