Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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 #42

Open
ThomasLatham opened this issue Nov 15, 2023 · 2 comments

Comments

@ThomasLatham
Copy link

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 ANDs 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:
image

What else have you tried?

  • I tried ANDing the conditions to see if that resulted in ORs in the result (in case there was a mix-up or something), but that gave me the same outcome as ORing.
  • 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 and AND operators), but this also didn't help.
  • I manually created the desired segment with ORed 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):
image

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.

@ThomasLatham
Copy link
Author

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.

@ThomasLatham
Copy link
Author

ThomasLatham commented Nov 15, 2023

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 ANDed together, rather than ORed.


I also think the quotation-mark behavior with the Contains operator in Expressions 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:
image

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:
image


Are these points that should be taken up in a PR? Or are they actually quite minor?

@ThomasLatham ThomasLatham reopened this Nov 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant