Tree Extraction for JavaScript Object Graphs
About
Extraction is a small JavaScript library for extracting object trees
from arbitrary object graphs. Object graphs usually have cycles and
contain many information. Hence, the clue is that the extracted object
trees use links to break reference cycles and can be just partial by
leaving out information. The Extraction library is intended for two
main use cases: to support the persisting and restoring of arbitrary
in-memory object graph structures (where the cycle problem has to be
resolved) and to support the generation of responses in REST APIs based
on object graphs (where the partial information has to be resolved).
Sneak Preview
var Graph = {
Person: [
{ id: 7, name: "God", tags: [ "good", "nice" ] },
{ id: 666, name: "Devil", tags: [ "bad", "cruel" ] } ],
Location: [
{ id: 0, name: "World" },
{ id: 1, name: "Heaven" },
{ id: 999, name: "Hell" } ] }
Graph.Person[0].home = Graph.Location[1]
Graph.Person[1].home = Graph.Location[2]
Graph.Person[1].rival = Graph.Person[0]
Graph.Person[0].rival = Graph.Person[1]
Graph.Location[0].subs = [ Graph.Location[1], Graph.Location[2] ]
Graph.Location[1].owner = Graph.Person[0]
Graph.Location[2].owner = Graph.Person[1]
import { extract } from "extraction"
let tree = extract(Graph.Person[0], "{ name, rival { home { name } }")
import { expect } from "chai"
expect(tree).to.be.deep.equal({ name: "God", rival: { home: { name: "Hell" } } })
Installation
Node environments (with NPM package manager):
$ npm install extraction
Browser environments (with Bower package manager):
$ bower install extraction
Usage
The Extraction library exposes two API functions (signatures given in TypeScript notation):
This is the main API method for extracting an object tree from an object
graph with the help of a tree extraction DSL.
extraction.extract(graph: object, spec: string, options?: object): object
-
The graph
argument has to be an Array of Object and be any start node in the graph.
-
The spec
argument is the tree extraction specification Domain-Specific Language (DSL).
It has to follow the following PEG-style grammar:
RHS | | LHS |
---|
spec | ::= | object / array |
object | ::= | "{" content? "}" |
array | ::= | "[" content? "]" |
content | ::= | ("->" num) / (field ("," field)*) |
field | ::= | (property spec) / ("!" ? property) |
property | ::= | id / "*" / (num ".." num) / num |
num | ::= | ("-" ? [0-9] +) / "-oo" / "oo" |
id | ::= | [$a-zA-Z_][$a-zA-Z0-9_] * |
Hint: the matching of multiple field
in content
follows a last-match semantic!
-
The options
argument is optional and can contain the following properties:
-
procValueBefore: (value: any) => any
:
Pre-process a value (object or property value) before it is taken into account.
A caller could use this to convert the value from a custom type into a standard
JavaScript type.
-
procValueAfter: (value: any) => any
:
Post-process a value (object or property value) after it was taken into account.
A caller could use this to convert the value into an external representation
like JSON or XML.
-
makeRefValue: (pathFirst: string, pathNow: string, obj: Object) => any
:
Make an object reference out of an object obj
, which is now found (again)
at path pathNow
and the first-time found at pathFirst
. The default
is to use pathFirst
as the reference, but a caller could also use
a stub for obj
(usually based on just the OID of it) as the reference.
-
debug: boolean
:
Print debug information about internal processing.
reify
This is a utility API method to re-generate an object graph from an
object tree by reifying all self-references back to the referenced
objects.
extraction.reify(tree: object, options?: object): object
Example
Suppose we have an object graph (aka "business model") based
on two entity definitions (in pseudo language):
Person {
id: number
name: string
tags: string+
home: Location
rival: Person?
}
Location {
id: number
name: string
owner: Person?
subs: Location*
}
A possible JavaScript instanciation of this object graph definition then
could be:
var Graph = {
Person: [
{ id: 7, name: "God", tags: [ "good", "nice" ] },
{ id: 666, name: "Devil", tags: [ "bad", "cruel" ] }
],
Location: [
{ id: 0, name: "World" },
{ id: 1, name: "Heaven" },
{ id: 999, name: "Hell" }
]
}
Graph.Person[0].home = Graph.Location[1]
Graph.Person[1].home = Graph.Location[2]
Graph.Person[1].rival = Graph.Person[0]
Graph.Person[0].rival = Graph.Person[1]
Graph.Location[0].subs = [ Graph.Location[1], Graph.Location[2] ]
Graph.Location[1].owner = Graph.Person[0]
Graph.Location[2].owner = Graph.Person[1]
Because of the relationship cycles in this graph, you cannot easily
serialize this graph as JSON with plain JSON.stringify()
as it
will detect but not handle the cycles correctly. With the Extraction library
you can serialize and deseralize this graph just fine:
import { extract, reify } from "extraction"
import { expect } from "chai"
import { inspect } from "util"
let tree = extract(Graph, "{ -> oo }")
console.log(inspect(tree, { depth: null }))
tree = JSON.parse(JSON.stringify(tree))
let GraphNew = reify(tree)
expect(GraphNew).to.be.deep.equal(Graph)
Now suppose we have a REST API where we want to let Persons
with their home Location be queried:
import HAPI from "hapi"
import { extract } from "./lib/extraction"
import Graph from "./sample-graph"
var server = new HAPI.Server()
server.connection({ address: "0.0.0.0", port: "12345" })
server.route({
method: "GET",
path: "/persons/{id}",
handler: (request, reply) => {
let id = parseInt(request.params.id)
let person = Graph.Person.find((person) => person.id === id)
let response = JSON.stringify(extract(
person, "{ id, name, home { id, name } }"
))
reply(response)
}
})
server.start((err) => {
if (err)
console.log(err)
})
Querying the two Persons yields:
$ curl http://127.0.0.1:12345/persons/7
{"id":7,"name":"God","home":{"id":1,"name":"Heaven"}}
$ curl http://127.0.0.1:12345/persons/6660
{"id":666,"name":"Devil","home":{"id":999,"name":"Hell"}}
Finally, instead of extracting a tree and then encoding it
as JSON, you can immediately encode it during extraction:
extraction.extract(Graph, "{ -> oo }", {
procValueAfter: (value) => {
if (typeof value === "object" && value !== null) {
if (value instanceof Array)
value = "[" + value.join(",") + "]"
else
value = "{" + Object.keys(value).map(function (key) {
return JSON.stringify(key) + ":" + value[key]
}).join(",") + "}"
}
else
value = JSON.stringify(value)
return value
}
}))
Implementation Notice
Although the Extraction library is written in ECMAScript 6, it is
transpiled to ECMAScript 5 and this way runs in really all(!) current
(as of 2015) JavaScript environments, of course.
License
Copyright (c) 2015 Ralf S. Engelschall (http://engelschall.com/)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.