CacheControl / json-rules-engine

A rules engine expressed in JSON
ISC License
2.54k stars 455 forks source link

Unexpected output with arrays and contains operator #186

Closed jorchg closed 4 years ago

jorchg commented 4 years ago

Hello everybody,

I have been trying this engine for a couple of days and I think is awesome! Congratulations on the effort of maintaining such a project.

Yesterday doing a test with some rules for my business case I realized about what for my is an unexpected output with contains operator.

I've made a POC https://codesandbox.io/s/gracious-elgamal-1od2s (maybe you have to login, but the code can be copied and pasted and run in a local environment).

I have this rule:

  {
    "id": "5f949cb6-38ad-11ea-a137-2e728ce88125",
    "conditions": {
      "any": [
        {
          "fact": "user",
          "operator": "contains",
          "value": "1",
          "path": "$.coursesDone[*].id"
        },
        {
          "fact": "user",
          "operator": "contains",
          "value": "8",
          "path": "$.coursesDone[*].id"
        },
        {
          "fact": "user",
          "operator": "contains",
          "value": "12",
          "path": "$.coursesDone[*].id"
        }
      ]
    },
    "event": {
      "type": "triggerModalWithUserId",
      "params": {
        "ruleId": "be41dd90-389a-11ea-a137-2e728ce88125",
        "html": "Correctly triggered"
      }
    },
    "createdAt": 1579116715227,
    "updatedAt": 157911671522
  }

And I have this three users:

[
  {
    "id": "74317052-3896-11ea-a137-2e728ce88125",
    "email": "Sincere@april.biz",
    "name": "Leanne Graham",
    "username": "Bret",
    "coursesDone": [
      {
        "id": "1",
        "name": "Blockchain for dummies",
        "section": "tecnologia",
        "subSection": "blockchain"
      },
      {
        "id": "2",
        "name": "Neuroventas: Ventas efectivas por Whatsapp",
        "section": "negocio",
        "subSection": "ventas"
      },
      {
        "id": "3",
        "name": "Qué es y cómo usar la blockchain de NEM",
        "section": "tecnologia",
        "subSection": "blockchain"
      }
    ],
    "coursesVisited": [
      {
        "id": "4",
        "name": "Curso Completo de Steemit y conviértete en Blogger",
        "section": "tecnologia",
        "subSection": "blockchain"
      },
      {
        "id": "5",
        "name": "Curso Bitcoin y Trading CriptoMonedas",
        "section": "tecnologia",
        "subSection": "blockchain"
      },
      {
        "id": "6",
        "name": "Blockchain Demo Day - abril 2019",
        "section": "tecnologia",
        "subSection": "blockchain"
      },
      {
        "id": "7",
        "name": "Master en Blockchain online - versión de prueba",
        "section": "tecnologia",
        "subSection": "blockchain"
      }
    ],
    "ranking": "heavy",
    "profile": "blockchain",
    "lastSeen": 1579203438850
  },
  {
    "id": "9312f0d4-3898-11ea-a137-2e728ce88125",
    "email": "Shanna@melissa.tv",
    "name": "Ervin Howell",
    "username": "Antonette",
    "coursesDone": [
      {
        "id": "8",
        "name": "Introducción al desarrollo de temas WordPress",
        "section": "productividad",
        "subSection": "Wordpress y CMS"
      }
    ],
    "coursesVisited": [
      {
        "id": "9",
        "name": "Experto en Microsoft Excel en tres pasos con HyperExcel",
        "section": "productividad",
        "subSection": "ofimatica"
      },
      {
        "id": "10",
        "name": "Aprende a usar Amazon Kindle y consigue gratis libros en español",
        "section": "productividad",
        "subSection": "Software de gestion"
      },
      {
        "id": "11",
        "name": "Tutorial Wordpress: cómo crear una página web fácilmente",
        "section": "productividad",
        "subSection": "Wordpress y CMS"
      }
    ],
    "ranking": "leecher",
    "profile": "productividad",
    "lastSeen": 1571255145000
  },
  {
    "id": "96eb8b08-3898-11ea-a137-2e728ce88125",
    "email": "Nathan@yesenia.net",
    "name": "Clementine Bauch",
    "username": "Samantha",
    "coursesDone": [
      {
        "id": "12",
        "name": "Funcionamiento del cerebro y Neuroeducación",
        "section": "cultura",
        "subSection": "educacion"
      },
      {
        "id": "13",
        "name": "Matemáticas desde cero. Nivel 1",
        "section": "cultura",
        "subSection": "educacion"
      }
    ],
    "coursesVisited": [
      {
        "id": "14",
        "name": "Facebook Ads - Conviertete en un experto",
        "section": "negocio",
        "subSection": "marketing"
      },
      {
        "id": "15",
        "name": "Curso Completo de Marketing y Tráfico Web con Redes Sociales",
        "section": "negocio",
        "subSection": "marketing"
      },
      {
        "id": "16",
        "name": "Primeros pasos para meditar y aprender a pensar en positivo",
        "section": "Ocio y Vida",
        "subSection": "Relax y Bienestar"
      },
      {
        "id": "17",
        "name": "Cinematografía para principiantes",
        "section": "cultura",
        "subSection": "cine"
      }
    ],
    "ranking": "average",
    "profile": "cultura",
    "lastSeen": 1576525545000
  }
]

Which are stored in DB, so I feed the engine with a fact like so:

  engine.addFact('user', function (params, almanac) {
    return almanac.factValue('userId')
      .then((userId) => {
        return getUserFromDb(userId);
      });
  });

So I have realized of an unexpected behaviour. When a user only has one element on his array of coursesDone the rule does not match, but if it has more than one element it correctly matches the rule.

Also, searching in your docs and examples if I have maybe misunderstood something I have realized that cloning the repo and executing the example ./examples/03-dynamic-facts.js does not output what it should output, and the condition that is failing also uses a contains operator.

Debug output of the example:

engine::addOperator name:equal
engine::addOperator name:notEqual
engine::addOperator name:in
engine::addOperator name:notIn
engine::addOperator name:contains
engine::addOperator name:doesNotContain
engine::addOperator name:lessThan
engine::addOperator name:lessThanInclusive
engine::addOperator name:greaterThan
engine::addOperator name:greaterThanInclusive
engine::addFact id:account-information
engine::run started
engine::run runtimeFacts:
almanac::constructor initialized runtime fact:accountId with lincoln<string>
almanac::constructor initialized runtime fact:success-events with undefined<undefined>
almanac::factValue cache miss for fact:account-information; calculating
condition::evaluate extracting object property $.company
almanac::factValue cache hit for fact:account-information
condition::evaluate extracting object property $.status
almanac::factValue cache hit for fact:account-information
condition::evaluate extracting object property $.ptoDaysTaken
loading account information for "lincoln"
condition::evaluate extracting object property $.company, received: microsoft
condition::evaluate extracting object property $.status, received: active
condition::evaluate extracting object property $.ptoDaysTaken, received: 2016-02-21,2016-12-25,2016-03-28
condition::evaluate <microsoft equal microsoft?> (true)
condition::evaluate <active in active,paid-leave?> (true)
condition::evaluate <2016-02-21,2016-12-25,2016-03-28 contains 2016-12-25?> (false) # This is the condition not being correctly evaluated
rule::evaluateConditions results
engine::run ruleResult:false
engine::run completed
almanac::factValue cache miss for fact:success-events; calculating

I haven't had time to take a look and see if this is a bug and maybe submitting a PR, but today I will be looking into this. Maybe an issue about JsonPath? I read a related issue where the package jsonpath-plus was not consistently returning an array when it should be, but seems like it was already fixed.

Thank you very much!

jorchg commented 4 years ago

In fact, if in your example I change the microsoftRule to use a selectn syntax it works like a charm:

const microsoftRule = {
  conditions: {
    all: [{
      fact: 'account-information',
      operator: 'equal',
      value: 'microsoft',
      path: '$.company' // access the 'company' property of "account-information"
    }, {
      fact: 'account-information',
      operator: 'in',
      value: ['active', 'paid-leave'], // 'status'' can be active or paid-leave
      path: '$.status' // access the 'status' property of "account-information"
    }, {
      fact: 'account-information',
      operator: 'contains',
      value: '2016-12-25',
      path: '.ptoDaysTaken' // access the 'ptoDaysTaken' property of "account-information"
    }]
  },
  event: {
    type: 'microsoft-christmas-pto',
    params: {
      message: 'current microsoft employee taking christmas day off'
    }
  }
}
engine::addOperator name:equal
engine::addOperator name:notEqual
engine::addOperator name:in
engine::addOperator name:notIn
engine::addOperator name:contains
engine::addOperator name:doesNotContain
engine::addOperator name:lessThan
engine::addOperator name:lessThanInclusive
engine::addOperator name:greaterThan
engine::addOperator name:greaterThanInclusive
engine::addFact id:account-information
engine::run started
engine::run runtimeFacts:
almanac::constructor initialized runtime fact:accountId with lincoln<string>
almanac::constructor initialized runtime fact:success-events with undefined<undefined>
almanac::factValue cache miss for fact:account-information; calculating
condition::evaluate extracting object property $.company
almanac::factValue cache hit for fact:account-information
condition::evaluate extracting object property $.status
almanac::factValue cache hit for fact:account-information
loading account information for "lincoln"
condition::evaluate extracting object property $.company, received: microsoft
condition::evaluate extracting object property $.status, received: active
condition::evaluate extracting object property .ptoDaysTaken, received: 2016-02-21,2016-12-25,2016-03-28
condition::evaluate <microsoft equal microsoft?> (true)
condition::evaluate <active in active,paid-leave?> (true)
condition::evaluate <2016-02-21,2016-12-25,2016-03-28 contains 2016-12-25?> (true)
rule::evaluateConditions results
engine::run ruleResult:true
almanac::factValue cache miss for fact:success-events; calculating
engine::run completed
almanac::factValue cache miss for fact:success-events; calculating
lincoln is a current microsoft employee taking christmas day off
jorchg commented 4 years ago

This is totally a issue with jsonpath-plus which incosistently returns an element when the input is an array with a single element, while if the input array contains more than one element it returns an array :S

I don't think that follows jsonpath specification. A common online checker does return an array when the input is an array. For example:

image

Should raise an issue on jsonpath-plus repository 👍🏻

jorchg commented 4 years ago

Oh I see this is fixed in https://github.com/s3u/JSONPath/issues/98 but v3.0.0 it's not yet published to npm. Hope there is an update soon.