json-logic-js icon indicating copy to clipboard operation
json-logic-js copied to clipboard

Is it possible to use JSONLogic as a query language?

Open FluorescentHallucinogen opened this issue 7 years ago • 8 comments

Is it possible to use JSONLogic as a query language to filter JSON? Something like this:

var data =
[
	{
		"id": 1,
		"name": "Oliver",
		"email": "[email protected]"
	},
	{
		"id": 2,
		"name": "Jack",
		"email": "[email protected]"
	},
	{
		"id": 3,
		"name": "Harry",
		"email": "[email protected]"
	},
	{
		"id": 4,
		"name": "Jacob",
		"email": "[email protected]"
	},
	{
		"id": 5,
		"name": "Charlie",
		"email": "[email protected]"
	},
	{
		"id": 6,
		"name": "Thomas",
		"email": "[email protected]"
	},
	{
		"id": 7,
		"name": "George",
		"email": "[email protected]"
	},
	{
		"id": 8,
		"name": "Oscar",
		"email": "[email protected]"
	},
	{
		"id": 9,
		"name": "James",
		"email": "[email protected]"
	},
	{
		"id": 10,
		"name": "William",
		"email": "[email protected]"
	}
]

var filter =
{"or": [
	{"and": [
		{">=": [
			{"var": "id"}, 3
		]},
		{"<=": [
			{"var": "id"}, 5
		]}
	]},
	{">": [
		{"var": "id"}, 7
	]}
]};

jsonLogic.filter(filter, data);

It should return:

[
	{
		"id": 3,
		"name": "Harry",
		"email": "[email protected]"
	},
	{
		"id": 4,
		"name": "Jacob",
		"email": "[email protected]"
	},
	{
		"id": 5,
		"name": "Charlie",
		"email": "[email protected]"
	},
	{
		"id": 8,
		"name": "Oscar",
		"email": "[email protected]"
	},
	{
		"id": 9,
		"name": "James",
		"email": "[email protected]"
	},
	{
		"id": 10,
		"name": "William",
		"email": "[email protected]"
	}
]

FluorescentHallucinogen avatar Nov 03 '16 06:11 FluorescentHallucinogen

You could do this by combining JavaScript's filter with that rule:

data.filter(function(item){
  return jsonLogic.apply(filter, item);
})

You could also monkey-patch this onto the library quickly like:

jsonLogic.filter = function(rule, data){
    if(typeof data.filter !== "function"){
        throw "jsonLogic.filter requires data be an Array";
    }
    return data.filter(function(item){
        return jsonLogic.apply(rule, item);
    });
};

By the way, there's a special three-argument syntax for < and <= that can simplify your rule:

{ "or": [
    { "<=": [ 3, { "var": "id" }, 5 ] },
    { ">": [ { "var": "id" }, 7 ] }
] };

I hesitate to bake this into the spec, just because I don't have time right now to write appropriate unit tests for it.

Does this get you where you need to go?

jwadhams avatar Nov 03 '16 14:11 jwadhams

Works like a charm!

You can test and compare different JSON query languages online at http://jsonquerytool.com.

Looks like JSON Logic can be used as a good alternative to them, because 1) the rule of query can be so complex as I want and 2) the query is a JSON itself.

I want to create a server that uses only JSON for everything: JSON database, JSON API (JSON Logic for query language and JSON Patch for sync changes between client and server), JSON Schema (http://json-schema.org) for validating submitted data and describing API.

Not sure that there is a ready solution that uses all these things together.

Have you heard anything about JSON Patch (http://jsonpatch.com)?

I'm very inspired by JSON Patch idea, but seems the syntax might be easier and cleaner in prefix notation and without "op". Something like this:

[
    {"replace": [{"path": "/baz"}, {"value": "boo"}]},
    {"add": [{"path": "/hello"}, {"value": "world"}]},
    {"remove": {"path": "/foo"}}
]

or even:

[
    {"replace": ["/baz", "boo"]},
    {"add": ["/hello", "world"]},
    {"remove": "/foo"}
]

instead of patch:

[
    {"op": "replace", "path": "/baz", "value": "boo"},
    {"op": "add", "path": "/hello", "value": "world"},
    {"op": "remove", "path": "/foo"}
]

The original JSON document:

{
    "baz": "qux",
    "foo": "bar"
}

The result:

{
    "baz": "boo",
    "hello": "world"
}

And looks like JSON Logic can be used instead of JSON Pointer to specify the parts of the document to operate on.

BTW, seems the syntax of JSON Logic might be also easier and cleaner without "var". Something like this:

var rules =
{
    "and": [
        {"<": ["temp", 110]},
        {"==": ["pie.filling", "apple"]}
    ]
};

instead of:

var rules =
{
    "and": [
        {"<": [{"var": "temp"}, 110]},
        {"==": [{"var": "pie.filling"}, "apple"]}
    ]
};
var data =
{
    "temp": 100,
    "pie": {
        "filling": "apple"
    }
};
jsonLogic.apply(rules, data);
// true

What do you think about all this?

FluorescentHallucinogen avatar Nov 04 '16 18:11 FluorescentHallucinogen

I think you're right, you could pretty quickly patch in replace and add and remove operations with add_operation that act directly on the data object.

I'm not familiar with JsonPointer, but I have used dotty--I just suggested a way to mooch functionality off that library into JsonLogic rules in this issue.

Re: cleaner syntax without var, in your example how does the parser understand that "temp" and "pie.filling" are data names, but "apple" is a literal string? It's totally doable in a command like {"remove":"/foo"} where that argument can't be anything but a variable reference, but I don't think it'll work everywhere.

jwadhams avatar Nov 10 '16 22:11 jwadhams

in your example how does the parser understand that "temp" and "pie.filling" are data names, but "apple" is a literal string?

There are 2 possible ways:

The first (not recommended because limits the flexibility):

  • Comparisons should always be between a variable and a string, and never between variables (and never between strings?).
  • The variable should always be on the left hand side of the comparison, and the string on the right.

The second:

  • Use $temp for variables and temp for strings.

Why I raised the issue of easier and cleaner syntax? What do you think about the syntax of https://npmjs.com/package/json-logic? It also uses "var" ("variables"), but it uses "operation". Why you don't use "operation" ("op")?

FluorescentHallucinogen avatar Nov 11 '16 12:11 FluorescentHallucinogen

What about "filter" operation instead of jsonLogic.filter function to filter JSON?

What about sorting (ordering) JSON?

FluorescentHallucinogen avatar Nov 11 '16 13:11 FluorescentHallucinogen

  1. How to retrieve the whole object (the key and the value):

{"name": "Oliver"}

from data:

{
    "id": 1,
    "name": "Oliver",
    "email": "[email protected]"
}

?

  1. How to retrieve the only needed objects:
[
    {
        "name": "Oliver",
        "email": "[email protected]"
    },
    {
        "name": "Jack",
        "email": "[email protected]"
    }
]

from data:

[
    {
        "id": 1,
        "name": "Oliver",
        "email": "[email protected]"
    },
    {
        "id": 2,
        "name": "Jack",
        "email": "[email protected]"
    }
]

?

FluorescentHallucinogen avatar Nov 11 '16 14:11 FluorescentHallucinogen

@jwadhams, any ideas?

FluorescentHallucinogen avatar Dec 04 '16 17:12 FluorescentHallucinogen

The title of the issue caught my attention.

I really like JSON Logic. It seems incredibly powerful.

I think the issue described here: https://github.com/jwadhams/json-logic-js/issues/116 and the related PR could make the objective of this ticket far easier.

Assuming the traverse flag gets added, one could do the following:

function json_filter(logic, data, jsonlogic) {
  return data.filter(function(item){
    // NOTE: the proposed implementation of traverse gives us back an array, so we de-array-ify it.
    return jsonlogic.apply(logic, item)[0];
  });
};

jsonLogic.add_operation('json_filter', (logic, data, jsonlogic) => {
  return json_filter(logic, data, jsonlogic);
}, {
  traverse: false
});

Then, I did the following:

Expand to view example jsonLogic.apply
jsonLogic.apply(
{"json_filter":{"or":[{"<=":[3,{"var":"id"},5]},{">":[{"var":"id"},7]}]}},
[
      {
          "id": 1,
          "name": "Oliver",
          "email": "[email protected]"
      },
      {
          "id": 2,
          "name": "Jack",
          "email": "[email protected]"
      },
      {
          "id": 3,
          "name": "Harry",
          "email": "[email protected]"
      },
      {
          "id": 4,
          "name": "Jacob",
          "email": "[email protected]"
      },
      {
          "id": 5,
          "name": "Charlie",
          "email": "[email protected]"
      },
      {
          "id": 6,
          "name": "Thomas",
          "email": "[email protected]"
      },
      {
          "id": 7,
          "name": "George",
          "email": "[email protected]"
      },
      {
          "id": 8,
          "name": "Oscar",
          "email": "[email protected]"
      },
      {
          "id": 9,
          "name": "James",
          "email": "[email protected]"
      },
      {
          "id": 10,
          "name": "William",
          "email": "[email protected]"
      }
  ]
);

Results:

[
    {
        "id": 3,
        "name": "Harry",
        "email": "[email protected]"
    },
    {
        "id": 4,
        "name": "Jacob",
        "email": "[email protected]"
    },
    {
        "id": 5,
        "name": "Charlie",
        "email": "[email protected]"
    },
    {
        "id": 8,
        "name": "Oscar",
        "email": "[email protected]"
    },
    {
        "id": 9,
        "name": "James",
        "email": "[email protected]"
    },
    {
        "id": 10,
        "name": "William",
        "email": "[email protected]"
    }
]

Pretty cool!

And, one more thing for kicks that is only tangentially related. I had the same thought regarding JSON Patch! It would be neat to be able to leverage JSON Patch within JSON Logic.

Well, you could do such a thing! I'm attaching an example.

Expand to see JSON Patch example
import * as jsonPatch from 'fast-json-patch';

function applyPatch(logic, data, jsonlogic) {
  return jsonPatch.applyOperation(data, logic[0]).newDocument;
}

jsonLogic.add_operation('jsonPatch.applyPatch', applyPatch, {
  traverse: false
});

jsonLogic.apply(
{"jsonPatch.applyPatch":[{"op":"replace","path":"/0/id","value":"99"}]},
// the data from earlier
);

Results:

[
    {
        "id": "99",
        "name": "Oliver",
        "email": "[email protected]"
    },

All that to say, if the traverse flag gets merged (or something similar), or if you patch your own version of JSON Logic, the sky is the limit because it won't traverse into your operation's logic.

josephdpurcell avatar Jan 22 '23 03:01 josephdpurcell