@wootapa/polygraph

Tests objects with queries constructed using logical and comparison operators.

Usage no npm install needed!

<script type="module">
  import wootapaPolygraph from 'https://cdn.skypack.dev/@wootapa/polygraph';
</script>

README

Polygraph

Polygraph is a library for testing objects given a query that is constructed using logical and comparison operators.

const jobs = [...];
const query = and().gt('salary', 20000).any('title', ['Developer', 'Astronaut']).done();
const interestingJobs = jobs.filter(job => query.evaluate(job));

Polygraph is quite flexible and can:

  • Construct complex queries easily
  • Combine many queries into one
  • Serialize and store it for later, or maybe push to a webworker.

Installation

New browsers, bundlers and node14+ (es):

$ npm install --save @wootapa/polygraph

For old browsers and node12 (umd):

<script src="https://unpkg.com/@wootapa/polygraph"></script>
// polygraph.and() ...

Methods

Constructors

  • and() - Creates new query with a root and logical operator.
  • or() - Creates new builder with a root or logical operator.
  • not() - Creates new builder with a root not logical operator.
  • fromJson(json) - Creates new builder from a serialized builder.
  • define(alias, function) - Defines a new operator.

Logical operators

  • and() - True if all child operators are true.
  • or() - True if one of the child operators are true.
  • not() - True if all child operators are false.

Comparison operators

  • equals(key, value) - True if object[key] equals to value. Alias=eq.
  • isNull(key) - True if object[key] is null or undefined. Alias=isnull.
  • greaterThan(key, value) - True if object[key] is greater than value. Alias=gt.
  • greaterThanEquals(key, value) - True if object[key] is greater or equal to value. Alias=gte.
  • lessThan(key, value) - True if object[key] is less than value. Alias=lt.
  • lessThanEquals(key, value) - True if object[key] is less or equal to value. Alias=lte.
  • like(key, value) - True if object[key] is like value (case sensitive). Use * as wildcard. Alias=like.
  • ilike(key, value) - True if object[key] is like value (case insensitive). Use * as wildcard. Alias=ilike.
  • any(key, values[]) - True if object[key] equals to any of the values.

Evaluation

  • evaluate(object) - Evaluates object. True if object passed all operators.

Logical traversal

  • done() - Moves up to root logical.
  • up() - Moves up to parent logical.
  • down() - Moves to first logical child.
  • next() - Moves to next logical sibling.
  • prev() - Moves to previous logical sibling.

Other

  • asJson() - Serializes to json. Restore with fromJson.
  • asTree() - Returns a more human readable tree.
  • clone() - Returns a deeply cloned builder.
  • clear() - Clears all operators and below.
  • operator(alias, key, value?, opts?) - Use operator by its alias.
  • op(alias, key, value?, opts?) - shorthand for above.
  • addBuilder(builder) - Adds another builder.
  • getReport() - Returns a report with statistics. Useful for finding the breaking operator or bottlenecks.
  • resetReport() - Reset statistics.
  • getKeysAndValues() - Returns keys and values for all comparison operators. This can be useful when restoring state to something (forms etc). If the same key has been used multiple times an array of values are returned.

An example

So maybe you have a bunch of movies and you want some good comedies.

const q = and().eq('category', 'comedy').gte('ranking', 6).done();

However, you just realized you have issues with some actors.

q.not().any('actor', ['Jim Carrey', 'Ben Stiller']).done();

Its better, but you heard there will be kids around.

q.eq('rating', 'G').done();

Also, you know your buddy likes the old movies better.

q.gt('year', new Date(1990,0)).lt('year', new Date(2000,0)).done();

In the end, this is the result.

const q = and()  // <- Will only return true if all children do
    .eq('category', 'comedy') // <- category must be comedy
    .gte('ranking', 6) // <- ranking must be greater than 6
    .not() // <- Will only return true if all children are false
        .any('actor', ['Jim Carrey', 'Ben Stiller']) // <- actors must not be these
        .up() // <- Moves up one level to 'and' operator
    .eq('rating', 'G') // <- rating must be G
    .gt('year', new Date(1990,0)) // <- year must be greater than 1990
    .lt('year', new Date(2000,0)) // <- year must be less than 2000
    .done() // <- moves to the root 'and' operator (which is unnecessary here but easy to forget)

const movies = [...];
const comedies = movies.filter(q.evaluate);

Custom operator

With define you can create your own operator. Supply an alias and a function that takes a value and returns a boolean.

define('divisibleby2', (value: number) => value % 2 === 0);

Then use it with the operator (or shorthand op) method.

and().op('divisibleby2', 'age').done().evaluate({ age: 20 }); // => true

The operator will survive serialization, but can break or have sideffects if you depend on this or some other state that might not be there after deserialization.

Aliases

All operators (except any) can be used with its alias, just as custom operators. It can be useful in situations where you simply want polygraph to figure out the operator for you. For example, the following is equivalent:

and().eq('name', 'Foo').gte('age', 20).done();
and().op('eq', 'name', 'Foo').op('gte', 'age', 20).done();

Objects

Polygraph tries to be somewhat intelligent, so the evaluating object can be a plain dict, have nested properties...or be a function. So, given the following:

const person = {
    name: {
        first: 'Nariyoshi',
        last: 'Miyagi'
    },
    age: () => 60
};

Nested properties will resolve.

and().eq('name.first', 'Nariyoshi').done().evaluate(person); // => true

Property functions also resolves.

and().gte('age', 50).done().evaluate(person); // => true

You can also pass a function and resolve values anyway you want. Key is passed as argument.

and().eq('name.first', 'Nariyoshi').done().evaluate(key => {
    if (key === 'name.first') {
        return person.name.first;
    }
}); // => true

Which then means you can have nonexistent properties if you want.

and().eq('isKarateMan', true).done().evaluate(key => {
    if (key === 'isKarateMan') {
        return person.name.first === 'Nariyoshi' && person.name.last === 'Miyagi';
    }
}); // => true