Skip to content

Commit

Permalink
feat(rstream-query): add obj->triple converter, update readme & example
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Apr 27, 2018
1 parent d03520d commit 6f95bcb
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 14 deletions.
34 changes: 26 additions & 8 deletions packages/rstream-query/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,30 +53,47 @@ yarn add @thi.ng/rstream-query
## Usage examples

```typescript
import { TripleStore, asTriples } from "@thi.ng/rstream-query";
import { trace } from "@thi.ng/rstream";
import { TripleStore } from "../src";

// create store with initial set of triples / facts
const store = new TripleStore([
["london", "type", "city"],
["london", "part-of", "uk"],
["portland", "type", "city"],
["portland", "part-of", "oregon"],
["portland", "part-of", "usa"],
["portland", "partOf", "oregon"],
["portland", "partOf", "usa"],
["oregon", "type", "state"],
["usa", "type", "country"],
["uk", "type", "country"],
]);

// alternatively, convert an object into a sequence of triples
const store = new TripleStore(asTriples({
london: {
type: "city",
partOf: "uk"
},
portland: {
type: "city",
partOf: ["oregon", "usa"]
},
oregon: { type: "state" },
uk: { type: "country" },
usa: { type: "country" },
});

// compile the below query spec into a dataflow graph
// pattern items prefixed w/ "?" are query variables

// this query matches the following relationships
// using all currently known triples in the store
// when matching triples are added or removed, the query
// result updates automatically...

// currently only "where" and "path" sub-queries are possible
// currently only "where" and bounded "path" sub-queries are possible
// in the near future, more query types will be supported
// (e.g. optional relationships, filters etc.)
// (e.g. optional relationships, pre/post filters etc.)
store.addQueryFromSpec({
q: [
{
Expand All @@ -85,7 +102,7 @@ store.addQueryFromSpec({
// match any subject of type "city"
["?city", "type", "city"],
// match each ?city var's "part-of" relationships (if any)
["?city", "part-of", "?country"],
["?city", "partOf", "?country"],
// matched ?country var must have type = "country"
["?country", "type", "country"]
]
Expand All @@ -100,7 +117,8 @@ store.addQueryFromSpec({
},
// another post-processing step, only keeps "answer" var in results
select: ["answer"]
}).subscribe(trace("results"))
})
.subscribe(trace("results"))
// results Set {
// { answer: 'london is located in uk' },
// { answer: 'portland is located in usa' } }
Expand All @@ -109,7 +127,7 @@ store.addQueryFromSpec({
const addCity = (name, country) =>
store.into([
[name, "type", "city"],
[name, "part-of", country],
[name, "partOf", country],
[country, "type", "country"],
]);

Expand Down
86 changes: 86 additions & 0 deletions packages/rstream-query/src/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { isArray } from "@thi.ng/checks/is-array";
import { isPlainObject } from "@thi.ng/checks/is-plain-object";
import { concat } from "@thi.ng/transducers/iter/concat";
import { pairs } from "@thi.ng/transducers/iter/pairs";
import { iterator } from "@thi.ng/transducers/iterator";
import { mapcat } from "@thi.ng/transducers/xform/mapcat";

let NEXT_ID = 0;

const mapBNode = (s: any, p: any, o: any) => {
const id = `__b${NEXT_ID++}__`;
return concat([[s, p, id]], asTriples(o, id));
};

const mapSubject = (subject: any) =>
([p, o]) => {
if (isArray(o)) {
return iterator(
mapcat((o) =>
isPlainObject(o) ?
mapBNode(subject, p, o) :
[[subject, p, o]]),
o);
} else if (isPlainObject(o)) {
return mapBNode(subject, p, o);
}
return [[subject, p, o]];
};

/**
* Converts given object into an iterable of triples, with the following
* conversion rules:
*
* - Toplevel object keys are used as subjects and MUST each have a
* plain object as value, where its keys are used as predicates and
* values as objects (in the SPO sense).
* - Plain objects in SPO object position are translated into unique IDs
* in order to allow the nested map to become a subject itself. In RDF
* terms, this is equivalent to BNodes.
* - Arrays in SPO object position cause multiple triples with same
* subject & predicate to be emitted. If any of the items in the array
* is a plain object, it will be treated as BNode and transformed as
* described in the previous rule
*
* ```
* src = {
* "@thi.ng/rstream-query": {
* type: "project",
* author: "toxi",
* tag: ["ES6", "TypeScript", "graph"]
* },
* toxi: {
* type: "person",
* hasAccount: [
* {type: "twitter", id: "toxi"},
* {type: "github", id: "postspectacular"}
* ]
* }
* };
*
* [...asTriples(src)]
* // [ [ '@thi.ng/rstream-query', 'type', 'project' ],
* // [ '@thi.ng/rstream-query', 'author', 'toxi' ],
* // [ '@thi.ng/rstream-query', 'tag', 'ES6' ],
* // [ '@thi.ng/rstream-query', 'tag', 'TypeScript' ],
* // [ '@thi.ng/rstream-query', 'tag', 'graph' ],
* // [ 'toxi', 'type', 'person' ],
* // [ 'toxi', 'hasAccount', '__b0__' ],
* // [ '__b0__', 'type', 'twitter' ],
* // [ '__b0__', 'id', 'toxi' ],
* // [ 'toxi', 'hasAccount', '__b1__' ],
* // [ '__b1__', 'type', 'github' ],
* // [ '__b1__', 'id', 'postspectacular' ] ]
* ```
*
* @param obj
* @param subject internal use only, do not specify!
*/
export const asTriples = (obj: any, subject?: any) =>
iterator(
mapcat(
subject === undefined ?
([s, v]: any) => iterator(mapcat(mapSubject(s)), <any>pairs(v)) :
mapSubject(subject)
),
pairs(obj));
1 change: 1 addition & 0 deletions packages/rstream-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./api";
export * from "./convert";
export * from "./pattern";
export * from "./qvar";
export * from "./store";
4 changes: 2 additions & 2 deletions packages/rstream-query/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export class TripleStore implements
}
}
return emitTriples ?
results.subscribe(asTriples(this)) :
results.subscribe(resultTriples(this)) :
results;
}

Expand Down Expand Up @@ -432,7 +432,7 @@ const indexSel = (key: any): Transducer<Edit, TripleIds> =>
);
};

const asTriples = (graph: TripleStore) =>
const resultTriples = (graph: TripleStore) =>
map<TripleIds, Set<Triple>>(
(ids) => {
const res = new Set<Triple>();
Expand Down
23 changes: 19 additions & 4 deletions packages/rstream-query/test/example.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { trace } from "@thi.ng/rstream";
import { TripleStore } from "../src";
import * as q from "../src";

const store = new TripleStore([
const store = new q.TripleStore([
["london", "type", "city"],
["london", "part-of", "uk"],
["portland", "type", "city"],
Expand All @@ -12,6 +12,21 @@ const store = new TripleStore([
["uk", "type", "country"],
]);

// alternatively, convert an object into a sequence of triples
// const store = new q.TripleStore(q.asTriples({
// london: {
// type: "city",
// partOf: "uk"
// },
// portland: {
// type: "city",
// partOf: ["oregon", "usa"]
// },
// oregon: { type: "state" },
// uk: { type: "country" },
// usa: { type: "country" },
// }));

// compile the below query spec into a dataflow graph
// pattern items prefixed w/ "?" are query variables

Expand All @@ -28,7 +43,7 @@ store.addQueryFromSpec({
// first match any subject of type "city"
["?city", "type", "city"],
// then a city's "part-of" relationships (if any)
["?city", "part-of", "?country"],
["?city", "partOf", "?country"],
// the matched ?country must have type = "country"
["?country", "type", "country"]
]
Expand All @@ -52,7 +67,7 @@ store.addQueryFromSpec({
const addCity = (name, country) =>
store.into([
[name, "type", "city"],
[name, "part-of", country],
[name, "partOf", country],
[country, "type", "country"],
]);

Expand Down

0 comments on commit 6f95bcb

Please sign in to comment.