{{pkg.description}}
IMPORTANT: This README is currently somewhat out-of-date and does not yet cover new important features introduced with version 2.2.0 onwards, please consult API docs (and/or source code) to view newly added functions and their usage...
This package provides a higher-order function defQuery()
, which takes a
number of options to configure query behavior and returns an actual query
function. This returned function can then be used for pattern matching of
objects and arrays of objects.
{{meta.status}}
Some of the below features are already partially addressed by other thi.ng/umbrella packages, but would benefit from a more unified approach.
- optional queries (OR queries)
- result projection
- result aggregation/grouping
{{repo.supportPackages}}
{{repo.relatedPackages}}
{{meta.blogPosts}}
{{pkg.install}}
{{pkg.size}}
{{pkg.deps}}
{{repo.examples}}
{{pkg.docs}}
Currently, there are 27 unique & optimized query implementations, each based on RDF-style Subject-Predicate-Object triple patterns (only without any similar restrictions on query terms data types), with each of the three terms one of:
null
- wildcard, any non-null (orundefined
) value in that position will be selected- Predicate function - called with all possible terms in that position
- Literal value - for subjects and predicates, this can only be a string or number. For "object" position any value type is allowed
- Array or
Set
- multiple choices (literals) for given query term
By default, arrays or sets in O(bject) position are matched in an elementwise manner using OR-semantics, i.e. a match succeeds with the first matched element. Since v0.3.0 intersection queries are supported too, i.e. all elements of the given array/set must match for the query to succeed (see examples/differences further below).
The behavior can be chosen via the intersect
query
option.
Object queries expect an object of the following structure:
{ subj1: { pred1: "obj1", pred2: 2, pred3: ["a", "b"] }, ... }
A concrete example:
const DB = {
alice: {
age: 33,
knows: ["bob", "charlie", "dori"],
type: "person",
},
bob: {
age: 32,
knows: ["alice"],
type: "person",
spouse: "alice",
},
charlie: {
parent: "alice",
knows: ["alice", "bob", "dori"],
},
dori: {
knows: ["bob"]
}
};
To find all answers for the question: Who knows Bob?
import { defQuery } from "@thi.ng/oquery";
// create query w/ custom options
// (options explained further below...)
const q = defQuery({ partial: true });
console.log(q(DB, null, "knows", "bob"));
// {
// alice: { knows: [ 'bob' ] },
// charlie: { knows: [ 'bob' ] },
// dori: { knows: [ 'bob' ] }
// }
For each of the 3 query terms, the following IDs are used:
*
= null (wildcard)l
= literal value (or array/set of literals)f
= predicate function
SPO pattern | Matches... |
---|---|
* * * |
everything |
* * l |
all objects with ANY property matching given literal |
* * f |
all objects with ANY property matching given predicate |
* l * |
objects with a property matching given literal |
* l l |
objects with a property AND its value matching given literals |
* l f |
objects with given property AND its value matching predicate |
* f * |
objects with properties matching given predicate |
* f l |
objects with properties matching given predicate AND their literal values |
* f f |
objects with properties matching given predicate AND their literal values |
Further variations:
(1) If the "subject" term is a literal (or array), then only object(s) for given key value(s) are matched, using the same logic for the other two terms as in the table above.
// Who does Alice know?
q(DB, "alice", "knows", null)
// { alice: { knows: [ 'bob', 'charlie', 'dori' ] } }
(2) If the subject is a predicate, then any top-level keys matching the given predicate will be matched (again using same rules as above for the other query terms).
// Anyone with initial "A" knows Charlie?
q(DB, (s) => s[0] === "a", "knows", "charlie")
// { alice: { knows: [ 'charlie' ] } }
(3) Instead of a root object (like DB
), an array of objects can be queried. In
this case, only predicate-object patterns are used (no subject terms, aka
array indices in this case).
type Person = { id: string; knows: string[] };
const DBALT: Person[] = [
{ id: "alice", knows: ["bob", "charlie"] },
{ id: "bob", knows: ["alice"] },
{ id: "charlie", knows: ["alice","bob","dori"] },
];
defQuery<Person[]>()(DBALT, "knows", "alice")
// [
// { id: 'bob', knows: [ 'alice' ] },
// { id: 'charlie', knows: [ 'alice', 'bob', 'dori' ] }
// ]
The following example is using the DB
object defined further
above...
import { defQuery } from "@thi.ng/oquery";
// using partial result objects option for brevity here
const q = defQuery({ partial: true });
// find all subjects with `type = "person"` relationship
q(DB, null, "type", "person");
// { alice: { type: 'person' }, bob: { type: 'person' } }
// everyone w/ given min age
q(DB, null, "age", (age) => age >= 33)
// { alice: { age: 33 } }
// select only subjects with A/B initials
q(DB, (id) => id >= "a" && id < "c", null, null)
// {
// alice: { age: 33, knows: [ 'bob', 'charlie', 'dori' ], type: 'person' },
// bob: { age: 32, knows: [ 'alice' ], type: 'person', spouse: 'alice' }
// }
Union vs. intersection queries:
const union = defQuery();
// who knows bob OR charlie?
union(DB, null, "knows", ["bob", "charlie"]);
// {
// alice: { age: 33, knows: [ 'bob', 'charlie', 'dori' ], type: 'person' },
// charlie: { parent: 'alice', knows: [ 'alice', 'bob', 'dori' ] },
// dori: { knows: [ 'bob' ] }
// }
const isec = defQuery({ intersect: true });
// who knows bob AND charlie?
isec(DB, null, "knows", ["bob", "charlie"]);
// {
// alice: { age: 33, knows: [ 'bob', 'charlie', 'dori' ], type: 'person' }
// }
More query examples in tests...