Skip to content

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

Description

@ThomasLatham

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions