├── .coveralls.yml
├── .eslintrc.json
├── .gitignore
├── .travis.yml
├── index.js
├── lib
├── HyperdbReadTransform.js
├── JoinStream.js
├── Variable.js
├── constants.js
├── planner.js
├── prefixes.js
└── utils.js
├── licenses
├── HYPER_GRAPH_DB_LICENSE
└── LEVELGRAPH_LICENSE
├── package-lock.json
├── package.json
├── readme.md
└── test
├── basic.spec.js
├── data
├── simplefoaf.ttl
└── sparqlIn11Minutes.ttl
├── fixture
├── foaf.js
└── homes_in_paris.js
├── join-stream.spec.js
├── planner.spec.js
├── prefixes.spec.js
├── queries
├── sparqlIn11Minutes1.rq
├── sparqlIn11Minutes11.rq
├── sparqlIn11Minutes2.rq
├── sparqlIn11Minutes3.rq
├── sparqlIn11Minutes4.rq
├── sparqlIn11Minutes5.rq
├── sparqlIn11Minutes6.rq
├── sparqlIn11Minutes7.rq
├── sparqlIn11Minutes8.rq
├── sparqlIn11Minutes9.rq
└── union.rq
├── sparql.spec.js
├── triple-store.spec.js
├── utils.spec.js
└── variable.spec.js
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: UOjQpeYk2baG2Ehj54CukhLdKBIN96Bya
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["standard"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | node_modules/
3 | *.log
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 6
4 | - 7
5 | - 8
6 | - 9
7 | - 10
8 | script:
9 | - npm run lint
10 | - npm run test
11 | jobs:
12 | include:
13 | - stage: coverage
14 | node_js: 8
15 | script: npm run travis
16 | after_script:
17 | - npm run report-coverage
18 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const hyperdb = require('hyperdb')
2 | const stream = require('readable-stream')
3 | const thunky = require('thunky')
4 | const pump = require('pump')
5 | const inherits = require('inherits')
6 | const events = require('events')
7 | const SparqlIterator = require('sparql-iterator')
8 |
9 | const constants = require('./lib/constants')
10 | const utils = require('./lib/utils')
11 | const prefixes = require('./lib/prefixes')
12 | const Variable = require('./lib/Variable')
13 | const HyperdbReadTransform = require('./lib/HyperdbReadTransform')
14 | const JoinStream = require('./lib/JoinStream')
15 | const planner = require('./lib/planner')
16 | const pkg = require('./package.json')
17 |
18 | const Transform = stream.Transform
19 | const PassThrough = stream.PassThrough
20 |
21 | function Graph (storage, key, opts) {
22 | if (!(this instanceof Graph)) return new Graph(storage, key, opts)
23 | events.EventEmitter.call(this)
24 | if (typeof key === 'string') key = Buffer.from(key, 'hex')
25 |
26 | if (!Buffer.isBuffer(key) && !opts) {
27 | opts = key
28 | key = null
29 | }
30 |
31 | opts = opts || {}
32 | this.db = (storage instanceof hyperdb) ? storage : hyperdb(storage, key, opts)
33 | this._prefixes = Object.assign({}, opts.prefixes || constants.DEFAULT_PREFIXES)
34 | this._basename = opts.name || constants.DEFAULT_BASE
35 | this._prefixes._ = this._basename
36 | this._indexType = opts.index === 'tri' ? 'tri' : 'hex'
37 | this._indexes = opts.index === 'tri'
38 | ? constants.HEXSTORE_INDEXES_REDUCED
39 | : constants.HEXSTORE_INDEXES
40 | this._indexKeys = Object.keys(this._indexes)
41 | this.isReady = false
42 | this.ready = thunky(this._ready.bind(this))
43 | this.db.on('error', (e) => {
44 | this.emit('error', e)
45 | })
46 | this.db.ready(() => {
47 | this.ready(() => {
48 | this.emit('ready')
49 | })
50 | })
51 | }
52 |
53 | inherits(Graph, events.EventEmitter)
54 |
55 | Graph.prototype._ready = function (cb) {
56 | this.isReady = true
57 | if (utils.isNewDatabase(this.db)) {
58 | this._onNew(cb)
59 | } else {
60 | this._onInit(cb)
61 | }
62 | }
63 |
64 | Graph.prototype._onNew = function (cb) {
65 | this._version = pkg.version
66 | const metadata = [
67 | ['@version', pkg.version],
68 | ['@index', this._indexType],
69 | ['@name', this._basename]
70 | ]
71 | Object.keys(this._prefixes).forEach((key) => {
72 | if (key !== '_') {
73 | metadata.push([prefixes.toKey(key), this._prefixes[key]])
74 | }
75 | })
76 | utils.put(this.db, metadata, cb)
77 | }
78 |
79 | Graph.prototype._onInit = function (cb) {
80 | // get and set graph version
81 | this._version = null
82 | this._basename = null
83 | this._indexType = null
84 |
85 | let missing = 4
86 | let error = null
87 | // get and set version
88 | this.graphVersion((err, version) => {
89 | if (err) error = err
90 | this._version = version
91 | maybeDone()
92 | })
93 | // get and set graph name
94 | this.name((err, name) => {
95 | if (err) error = err
96 | this._basename = name || constants.DEFAULT_BASE
97 | // modify prefixes to ensure correct namespacing
98 | this._prefixes._ = this._basename
99 | maybeDone()
100 | })
101 | // get and set graph indexation
102 | this.indexType((err, index) => {
103 | if (err) error = err
104 | this._indexType = index || 'hex'
105 | this._indexes = index === 'tri'
106 | ? constants.HEXSTORE_INDEXES_REDUCED
107 | : constants.HEXSTORE_INDEXES
108 | this._indexKeys = Object.keys(this._indexes)
109 | maybeDone()
110 | })
111 | // get and set prefixes
112 | this.prefixes((err, prefixes) => {
113 | if (err) error = err
114 | this._prefixes = Object.assign({ _: this._basename }, prefixes)
115 | maybeDone()
116 | })
117 | function maybeDone () {
118 | missing--
119 | if (!missing) {
120 | cb(error)
121 | }
122 | }
123 | }
124 |
125 | Graph.prototype.v = (name) => new Variable(name)
126 |
127 | function returnValueAsString (cb) {
128 | return (err, nodes) => {
129 | if (err) return cb(err)
130 | if (!nodes || nodes.length === 0) return cb(null, null)
131 | cb(null, nodes[0].value.toString())
132 | }
133 | }
134 |
135 | Graph.prototype.graphVersion = function (cb) {
136 | if (this._version) return cb(null, this._version)
137 | this.db.get('@version', returnValueAsString(cb))
138 | }
139 |
140 | Graph.prototype.name = function (cb) {
141 | if (this._basename) return cb(null, this._basename)
142 | this.db.get('@name', returnValueAsString(cb))
143 | }
144 |
145 | Graph.prototype.indexType = function (cb) {
146 | if (this._indexType) return cb(null, this._indexType)
147 | this.db.get('@index', returnValueAsString(cb))
148 | }
149 |
150 | Graph.prototype.prefixes = function (callback) {
151 | // should cache this somehow
152 | const prefixStream = this.db.createReadStream(constants.PREFIX_KEY)
153 | utils.collect(prefixStream, (err, data) => {
154 | if (err) return callback(err)
155 | var names = data.reduce((p, nodes) => {
156 | var data = prefixes.fromNodes(nodes)
157 | p[data.prefix] = data.uri
158 | return p
159 | }, {})
160 | callback(null, names)
161 | })
162 | }
163 |
164 | Graph.prototype.addPrefix = function (prefix, uri, cb) {
165 | this.db.put(prefixes.toKey(prefix), uri, cb)
166 | }
167 |
168 | Graph.prototype.getStream = function (triple, opts) {
169 | const stream = this.db.createReadStream(this._createQuery(triple, { encode: (!opts || opts.encode === undefined) ? true : opts.encode }))
170 | return stream.pipe(new HyperdbReadTransform(this.db, this._basename, opts))
171 | }
172 |
173 | Graph.prototype.get = function (triple, opts, callback) {
174 | if (typeof opts === 'function') return this.get(triple, undefined, opts)
175 | this.ready(() => {
176 | utils.collect(this.getStream(triple, opts), callback)
177 | })
178 | }
179 | function doAction (action) {
180 | return function (triples, callback) {
181 | if (!triples) return callback(new Error('Must pass triple'))
182 | this.ready(() => {
183 | let entries = (!triples.reduce) ? [triples] : triples
184 | entries = entries.reduce((prev, triple) => {
185 | return prev.concat(this._generateBatch(triple, action))
186 | }, [])
187 | this.db.batch(entries.reverse(), callback)
188 | })
189 | }
190 | }
191 |
192 | function doActionStream (action) {
193 | return function () {
194 | const self = this
195 | const transform = new Transform({
196 | objectMode: true,
197 | transform (triples, encoding, done) {
198 | if (!triples) return done()
199 | let entries = (!triples.reduce) ? [triples] : triples
200 | entries = entries.reduce((prev, triple) => {
201 | return prev.concat(self._generateBatch(triple, action))
202 | }, [])
203 | this.push(entries.reverse())
204 | done()
205 | }
206 | })
207 | const writeStream = this.db.createWriteStream()
208 | transform.pipe(writeStream)
209 | return transform
210 | }
211 | }
212 |
213 | Graph.prototype.put = doAction('put')
214 | Graph.prototype.putStream = doActionStream('put')
215 |
216 | // this is not implemented in hyperdb yet
217 | // for now we just put a null value in the db
218 | Graph.prototype.del = doAction('del')
219 | Graph.prototype.delStream = doActionStream('del')
220 |
221 | Graph.prototype.searchStream = function (query, options) {
222 | const result = new PassThrough({ objectMode: true })
223 | const defaults = { solution: {} }
224 | if (!query || query.length === 0) {
225 | result.end()
226 | return result
227 | } else if (!Array.isArray(query)) {
228 | query = [ query ]
229 | }
230 | const plannedQuery = planner(query, this._prefixes)
231 | var streams = plannedQuery.map((triple, i) => {
232 | const limit = (options && i === plannedQuery.length - 1) ? options.limit : undefined
233 | return new JoinStream({
234 | triple: utils.filterTriple(triple),
235 | filter: triple.filter,
236 | db: this,
237 | limit
238 | })
239 | })
240 |
241 | streams[0].start = true
242 | streams[0].end(defaults.solution)
243 |
244 | streams.push(result)
245 | pump(streams)
246 | return result
247 | }
248 |
249 | Graph.prototype.search = function (query, options, callback) {
250 | if (typeof options === 'function') {
251 | callback = options
252 | options = undefined
253 | }
254 | this.ready(() => {
255 | utils.collect(this.searchStream(query, options), callback)
256 | })
257 | }
258 |
259 | Graph.prototype.queryStream = function (query) {
260 | return new SparqlIterator(query, { hypergraph: this })
261 | }
262 |
263 | Graph.prototype.query = function (query, callback) {
264 | this.ready(() => {
265 | utils.collect(this.queryStream(query), callback)
266 | })
267 | }
268 |
269 | Graph.prototype.close = function (callback) {
270 | callback()
271 | }
272 |
273 | /* PRIVATE FUNCTIONS */
274 |
275 | Graph.prototype._generateBatch = function (triple, action) {
276 | if (!action) action = 'put'
277 | var data = null
278 | if (action === 'put') {
279 | data = JSON.stringify(utils.extraDataMask(triple))
280 | }
281 | return this._encodeKeys(triple).map(key => ({
282 | type: action,
283 | key: key,
284 | value: data
285 | }))
286 | }
287 |
288 | Graph.prototype._encodeKeys = function (triple) {
289 | const encodedTriple = utils.encodeTriple(triple, this._prefixes)
290 | return this._indexKeys.map(key => utils.encodeKey(key, encodedTriple))
291 | }
292 |
293 | Graph.prototype._createQuery = function (pattern, options) {
294 | var types = utils.typesFromPattern(pattern)
295 | var preferedIndex = options && options.index
296 | var index = this._findIndex(types, preferedIndex)
297 | const encodedTriple = utils.encodeTriple(pattern, options.encode ? this._prefixes : { _: this._basename })
298 | var key = utils.encodeKey(index, encodedTriple)
299 | return key
300 | }
301 |
302 | Graph.prototype._possibleIndexes = function (types) {
303 | var result = this._indexKeys.filter((key) => {
304 | var matches = 0
305 | return this._indexes[key].every(function (e, i) {
306 | if (types.indexOf(e) >= 0) {
307 | matches++
308 | return true
309 | }
310 | if (matches === types.length) {
311 | return true
312 | }
313 | })
314 | })
315 | result.sort()
316 | return result
317 | }
318 |
319 | Graph.prototype._findIndex = function (types, preferedIndex) {
320 | var result = this._possibleIndexes(types)
321 | if (preferedIndex && result.some(r => r === preferedIndex)) {
322 | return preferedIndex
323 | }
324 | return result[0]
325 | }
326 |
327 | module.exports = Graph
328 |
--------------------------------------------------------------------------------
/lib/HyperdbReadTransform.js:
--------------------------------------------------------------------------------
1 | const Transform = require('readable-stream').Transform
2 | const inherits = require('inherits')
3 | const utils = require('./utils')
4 |
5 | function HyperdbReadTransform (db, basename, options) {
6 | if (!(this instanceof HyperdbReadTransform)) {
7 | return new HyperdbReadTransform(db, basename, options)
8 | }
9 | var opts = options || {}
10 | this.db = db
11 | this._prefixes = { _: basename }
12 | this._count = 0
13 | this._filter = opts.filter
14 | this._offset = opts.offset || 0
15 | this._limit = opts.limit && opts.limit + this._offset
16 | Transform.call(this, Object.assign(opts, { objectMode: true }))
17 | this._sources = []
18 | this.once('pipe', (source) => {
19 | source.on('error', e => this.emit('error', e))
20 | this._sources.push(source)
21 | })
22 | }
23 |
24 | inherits(HyperdbReadTransform, Transform)
25 |
26 | HyperdbReadTransform.prototype._transform = function transform (nodes, encoding, done) {
27 | var value = nodes[0].value && JSON.parse(nodes[0].value.toString())
28 | if (value === null) return done()
29 | value = Object.assign(value, utils.decodeKey(nodes[0].key, this._prefixes))
30 | if (!this._filter || this._filter(value)) {
31 | if (this._count >= this._offset) {
32 | this.push(value)
33 | }
34 | this._count++
35 | if (this._limit && this._count >= this._limit) {
36 | this.end()
37 | }
38 | }
39 | done()
40 | }
41 |
42 | HyperdbReadTransform.prototype._flush = function (done) {
43 | this._sources.forEach(source => source.destroy())
44 | done()
45 | }
46 |
47 | module.exports = HyperdbReadTransform
48 |
--------------------------------------------------------------------------------
/lib/JoinStream.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2013-2017 Matteo Collina and LevelGraph Contributors
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
20 | OTHER DEALINGS IN THE SOFTWARE.
21 | */
22 |
23 | const Transform = require('readable-stream').Transform
24 | const inherits = require('inherits')
25 | const utils = require('./utils')
26 | const queryMask = utils.queryMask
27 | const maskUpdater = utils.maskUpdater
28 | const matcher = utils.matcher
29 |
30 | function JoinStream (options) {
31 | if (!(this instanceof JoinStream)) {
32 | return new JoinStream(options)
33 | }
34 | options.objectMode = true
35 | Transform.call(this, options)
36 |
37 | this.triple = options.triple
38 | this.matcher = matcher(options.triple)
39 | this.mask = queryMask(options.triple)
40 | this.maskUpdater = maskUpdater(options.triple)
41 | this.limit = options.limit
42 | this._limitCounter = 0
43 | this.db = options.db
44 | this._ended = false
45 | this.filter = options.filter
46 | this.offset = options.offset
47 |
48 | this.once('pipe', (source) => {
49 | source.on('error', (err) => {
50 | this.emit('error', err)
51 | })
52 | })
53 |
54 | this._onErrorStream = (err) => {
55 | this.emit('error', err)
56 | }
57 |
58 | this._onDataStream = (triple) => {
59 | var newsolution = this.matcher(this._lastSolution, triple)
60 |
61 | if (this._ended || !newsolution) {
62 | return
63 | }
64 |
65 | this.push(newsolution)
66 | this._limitCounter += 1
67 | if (this.limit && this._limitCounter === this.limit) {
68 | this._readStream.destroy()
69 | this._ended = true
70 | this.push(null)
71 | }
72 | }
73 |
74 | this._options = {
75 | filter: this.filter,
76 | offset: this.offset,
77 | encode: options.encode ? !!options.encode : false
78 | }
79 | }
80 |
81 | inherits(JoinStream, Transform)
82 |
83 | JoinStream.prototype._transform = function transform (solution, encoding, done) {
84 | if (this._ended) {
85 | return done()
86 | }
87 | var newMask = this.maskUpdater(solution, this.mask)
88 |
89 | this._lastSolution = solution
90 | this._readStream = this.db.getStream(newMask, this._options)
91 |
92 | this._readStream.on('data', this._onDataStream)
93 | this._readStream.on('error', this._onErrorStream)
94 | this._readStream.on('end', done)
95 | }
96 |
97 | module.exports = JoinStream
98 |
--------------------------------------------------------------------------------
/lib/Variable.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2013-2017 Matteo Collina and LevelGraph Contributors
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
20 | OTHER DEALINGS IN THE SOFTWARE.
21 | */
22 |
23 | function Variable (name) {
24 | if (!(this instanceof Variable)) return new Variable(name)
25 | this.name = name
26 | }
27 |
28 | Variable.prototype.bind = function (solution, value) {
29 | if (!this.isBindable(solution, value)) return null
30 | var newsolution = Object.assign({}, solution)
31 | newsolution[this.name] = value
32 | return newsolution
33 | }
34 |
35 | Variable.prototype.isBound = function (solution) {
36 | return solution[this.name] !== undefined
37 | }
38 |
39 | Variable.prototype.isBindable = function (solution, value) {
40 | return !solution[this.name] || solution[this.name] === value
41 | }
42 |
43 | module.exports = Variable
44 |
--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
1 | const HEXSTORE_INDEXES = {
2 | spo: ['subject', 'predicate', 'object'],
3 | pos: ['predicate', 'object', 'subject'],
4 | osp: ['object', 'subject', 'predicate'],
5 | sop: ['subject', 'object', 'predicate'], // [optional]
6 | pso: ['predicate', 'subject', 'object'], // [optional]
7 | ops: ['object', 'predicate', 'subject'] // [optional]
8 | }
9 | const HEXSTORE_INDEXES_REDUCED = {
10 | spo: HEXSTORE_INDEXES.spo,
11 | pos: HEXSTORE_INDEXES.pos,
12 | osp: HEXSTORE_INDEXES.osp
13 | }
14 | const PREFIX_KEY = '@prefix/'
15 | const DEFAULT_BASE = 'hg://'
16 | const DEFAULT_PREFIXES = {
17 | foaf: 'http://xmlns.com/foaf/0.1/'
18 | }
19 |
20 | module.exports = {
21 | PREFIX_KEY,
22 | DEFAULT_BASE,
23 | DEFAULT_PREFIXES,
24 | HEXSTORE_INDEXES,
25 | HEXSTORE_INDEXES_REDUCED
26 | }
27 |
--------------------------------------------------------------------------------
/lib/planner.js:
--------------------------------------------------------------------------------
1 |
2 | const utils = require('./utils')
3 |
4 | function planner (query, prefixMap) {
5 | if (query.length === 1) return [utils.encodeTriple(query[0], prefixMap)]
6 | // first group queries based on number of variables.
7 | const solved = new Set()
8 | var grouped = query.reduce((ordered, q) => {
9 | const variables = utils.variableNames(q)
10 | if (ordered[variables.length]) {
11 | ordered[variables.length].push({
12 | query: q,
13 | names: variables
14 | })
15 | } else {
16 | ordered[variables.length] = [{
17 | query: q,
18 | names: variables
19 | }]
20 | }
21 | if (variables.length === 1) {
22 | solved.add(variables[0])
23 | }
24 | return ordered
25 | }, [])
26 | // then order vars > 1 by if they occur in
27 | const orderedQueries = grouped[1] ? grouped[1].map(v => utils.encodeTriple(v.query, prefixMap)) : []
28 |
29 | for (let i = 2; i < grouped.length; i++) {
30 | if (grouped[i] === undefined) continue
31 | while (grouped[i].length > 0) {
32 | // get the next easiest to solve
33 | // or the one that makes the rest easiest to solve
34 | grouped[i].sort((a, b) => {
35 | // number of unsolved variables
36 | let unsolvedA = a.names.filter(name => !solved.has(name))
37 | let unsolvedB = b.names.filter(name => !solved.has(name))
38 | if (unsolvedA.length < unsolvedB.length) return -1
39 | if (unsolvedA.length > unsolvedB.length) return 1
40 | // calculate how many unsolved vars it has in common with others in the group
41 | // should this be a vector? many vars is better than solving 1 lots.
42 | let sharedUnsolvedA = 0
43 | let sharedUnsolvedB = 0
44 | grouped[i].forEach(v => {
45 | v.names.forEach((name) => {
46 | if (solved.has(name)) return
47 | if (v !== a && a.names.includes(name)) sharedUnsolvedA++
48 | if (v !== b && b.names.includes(name)) sharedUnsolvedB++
49 | })
50 | })
51 | if (sharedUnsolvedA > sharedUnsolvedB) return -1
52 | if (sharedUnsolvedA < sharedUnsolvedB) return 1
53 | return 0
54 | })
55 | const next = grouped[i].shift()
56 | orderedQueries.push(utils.encodeTriple(next.query, prefixMap))
57 | next.names.forEach(n => solved.add(n))
58 | }
59 | }
60 | return orderedQueries
61 | };
62 |
63 | module.exports = planner
64 |
--------------------------------------------------------------------------------
/lib/prefixes.js:
--------------------------------------------------------------------------------
1 | var constants = require('./constants')
2 |
3 | const PREFIX_KEY = constants.PREFIX_KEY
4 | const PREFIX_REGEX = /^(\w+):/
5 |
6 | function toKey (prefix) {
7 | return PREFIX_KEY + prefix
8 | }
9 | function fromKey (key) {
10 | return key.replace(PREFIX_KEY, '')
11 | }
12 |
13 | function fromNodes (nodes) {
14 | return {
15 | prefix: fromKey(nodes[0].key),
16 | uri: nodes[0].value.toString()
17 | }
18 | }
19 |
20 | function toPrefixed (uri, prefixes) {
21 | if (!prefixes) return uri
22 | const prefix = Object.keys(prefixes).find(v => uri.startsWith(prefixes[v]))
23 | if (!prefix) return uri
24 | return uri.replace(prefixes[prefix], prefix + ':')
25 | }
26 |
27 | function fromPrefixed (uri, prefixes) {
28 | if (!prefixes) return uri
29 | const match = uri.match(PREFIX_REGEX)
30 | if (match && prefixes[match[1]]) {
31 | return uri.replace(PREFIX_REGEX, prefixes[match[1]])
32 | }
33 | return uri
34 | }
35 |
36 | module.exports = {
37 | toKey,
38 | fromKey,
39 | fromNodes,
40 | toPrefixed,
41 | fromPrefixed
42 | }
43 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | const Variable = require('./Variable')
2 | const prefixes = require('./prefixes')
3 | const constants = require('./constants')
4 |
5 | const spo = constants.HEXSTORE_INDEXES.spo
6 | const tripleAliasMap = {
7 | s: 'subject',
8 | p: 'predicate',
9 | o: 'object'
10 | }
11 |
12 | function isNewDatabase (db) {
13 | return !(db.feeds.length > 1 || db.feeds[0].length > 1)
14 | }
15 |
16 | function put (db, data, callback) {
17 | var i = 0
18 |
19 | next()
20 |
21 | function next (err) {
22 | if (err) return callback(err)
23 | var v = data[i++]
24 | db.put(v[0], v[1], (i < data.length) ? next : callback)
25 | }
26 | }
27 |
28 | function collect (stream, cb) {
29 | var res = []
30 | stream.on('data', res.push.bind(res))
31 | stream.once('error', cb)
32 | stream.once('end', cb.bind(null, null, res))
33 | }
34 |
35 | function filterTriple (triple) {
36 | const filtered = {}
37 | spo.forEach((key) => {
38 | if (triple.hasOwnProperty(key)) {
39 | filtered[key] = triple[key]
40 | }
41 | })
42 | return filtered
43 | }
44 |
45 | function escapeKeyValue (value, prefixMap) {
46 | if (typeof value === 'string' || value instanceof String) {
47 | return prefixes.toPrefixed(value, prefixMap).replace(/(\/)/g, '%2F')
48 | }
49 | return value
50 | }
51 |
52 | function unescapeKeyValue (value, prefixMap) {
53 | return prefixes.fromPrefixed(value.replace(/%2F/g, '/'), prefixMap)
54 | }
55 |
56 | function encodeTriple (triple, prefixMap) {
57 | spo.forEach((key) => {
58 | if (triple.hasOwnProperty(key)) {
59 | triple[key] = escapeKeyValue(triple[key], prefixMap)
60 | }
61 | })
62 | return triple
63 | }
64 |
65 | function encodeKey (key, triple) {
66 | var result = key
67 | var def = constants.HEXSTORE_INDEXES[key]
68 | var i = 0
69 | var value = triple[def[i]]
70 | // need to handle this smarter
71 | while (value) {
72 | result += '/' + value
73 | i += 1
74 | value = triple[def[i]]
75 | }
76 | if (i < 3) {
77 | result += '/'
78 | }
79 | return result
80 | }
81 |
82 | function decodeKey (key, prefixMap) {
83 | const values = key.split('/')
84 | if (values.length < 4) throw new Error('Key is not in triple form')
85 | const order = values[0]
86 | const triple = {}
87 | for (var i = 0; i < 3; i++) {
88 | const k = tripleAliasMap[order[i]]
89 | triple[k] = unescapeKeyValue(values[i + 1], prefixMap)
90 | }
91 | return triple
92 | }
93 |
94 | function typesFromPattern (pattern) {
95 | return Object.keys(pattern).filter((key) => {
96 | switch (key) {
97 | case 'subject':
98 | return !!pattern.subject
99 | case 'predicate':
100 | return !!pattern.predicate
101 | case 'object':
102 | return !!pattern.object
103 | default:
104 | return false
105 | }
106 | })
107 | }
108 |
109 | function hasKey (key) {
110 | return spo.indexOf(key) >= 0
111 | }
112 |
113 | function keyIsNotAObject (tripleKey) {
114 | return typeof tripleKey !== 'object'
115 | }
116 |
117 | function keyIsAVariable (tripleKey) {
118 | return tripleKey instanceof Variable
119 | }
120 |
121 | function extraDataMask (obj) {
122 | return Object.keys(obj)
123 | .reduce((prev, key) => {
124 | if (!keyIsAVariable(obj[key]) && !hasKey(key)) prev[key] = obj[key]
125 | return prev
126 | }, {})
127 | }
128 |
129 | function objectMask (criteria, obj) {
130 | return Object.keys(obj)
131 | .filter(hasKey)
132 | .filter(key => criteria(obj[key]))
133 | .reduce((prev, key) => {
134 | prev[key] = obj[key]
135 | return prev
136 | }, {})
137 | };
138 |
139 | function variableNames (obj) {
140 | return Object.keys(obj)
141 | .filter(key => hasKey(key) && keyIsAVariable(obj[key]))
142 | .map(key => obj[key].name)
143 | };
144 |
145 | function queryMask (object) {
146 | return objectMask(keyIsNotAObject, object)
147 | };
148 |
149 | function variablesMask (object) {
150 | return objectMask(keyIsAVariable, object)
151 | };
152 |
153 | function maskUpdater (pattern) {
154 | const variables = variablesMask(pattern)
155 | return (solution, mask) => {
156 | const maskCopy = Object.assign({}, mask)
157 | return Object.keys(variables)
158 | .reduce((newMask, key) => {
159 | const variable = variables[key]
160 | if (variable.isBound(solution)) {
161 | newMask[key] = solution[variable.name]
162 | }
163 | return newMask
164 | }, maskCopy)
165 | }
166 | }
167 |
168 | function matcher (pattern) {
169 | const variables = variablesMask(pattern)
170 |
171 | return (solution, triple) => {
172 | return Object.keys(variables).reduce((newSolution, key) => {
173 | if (newSolution) {
174 | return variables[key].bind(newSolution, triple[key])
175 | }
176 | return newSolution
177 | }, solution)
178 | }
179 | }
180 |
181 | function materializer (pattern, data) {
182 | return Object.keys(pattern).reduce((result, key) => {
183 | if (pattern[key] instanceof Variable) {
184 | result[key] = data[pattern[key].name]
185 | } else {
186 | result[key] = pattern[key]
187 | }
188 | return result
189 | }, {})
190 | }
191 |
192 | module.exports = {
193 | isNewDatabase,
194 | put,
195 | escapeKeyValue,
196 | unescapeKeyValue,
197 | encodeTriple,
198 | encodeKey,
199 | decodeKey,
200 | typesFromPattern,
201 | filterTriple,
202 | collect,
203 | extraDataMask,
204 | queryMask,
205 | variablesMask,
206 | variableNames,
207 | maskUpdater,
208 | matcher,
209 | materializer
210 | }
211 |
--------------------------------------------------------------------------------
/licenses/HYPER_GRAPH_DB_LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Benjamin Forster and Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/licenses/LEVELGRAPH_LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2013-2017 Matteo Collina and LevelGraph Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hyper-graph-db",
3 | "version": "0.3.5",
4 | "description": "A distributed graph database built upon hyperdb.",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/e-e-e/hyper-graph-db.git"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/e-e-e/hyper-graph-db/issues"
12 | },
13 | "dependencies": {
14 | "hyperdb": "^3.5.0",
15 | "inherits": "^2.0.3",
16 | "lru": "^3.1.0",
17 | "pump": "^3.0.0",
18 | "readable-stream": "^3.0.1",
19 | "sparql-iterator": "^2.0.5",
20 | "thunky": "^1.0.2"
21 | },
22 | "devDependencies": {
23 | "chai": "^4.1.2",
24 | "coveralls": "^3.0.0",
25 | "istanbul": "^0.4.5",
26 | "mocha": "^5.0.0",
27 | "n3": "^0.8.5",
28 | "random-access-memory": "^3.0.0",
29 | "standard": "^12.0.0",
30 | "tmp": "0.0.33"
31 | },
32 | "scripts": {
33 | "test": "mocha test/**.spec.js",
34 | "tdd": "mocha test/**.spec.js -w",
35 | "lint": "standard",
36 | "lint:fix": "standard --fix",
37 | "travis": "NODE_ENV=test istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec",
38 | "report-coverage": "cat ./coverage/lcov.info | coveralls"
39 | },
40 | "author": "Benjamin Forster",
41 | "license": "MIT"
42 | }
43 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # hyper-graph-db
2 |
3 | [](https://greenkeeper.io/)
4 |
5 | [](https://travis-ci.org/e-e-e/hyper-graph-db) [](https://coveralls.io/github/e-e-e/hyper-graph-db?branch=master)
6 |
7 | hyper-graph-db is a graph database on top of [hyperdb](https://github.com/mafintosh/hyperdb). It interface and test specs have been adapted from [LevelGraph](https://github.com/levelgraph/levelgraph).
8 |
9 | Like LevelGraph, **hyper-graph-db** follows the **Hexastore** approach as presented in the article: [Hexastore: sextuple indexing for semantic web data management C Weiss, P Karras, A Bernstein - Proceedings of the VLDB Endowment, 2008](http://www.vldb.org/pvldb/1/1453965.pdf). As such hyper-graph-db uses six indices for every triple in order to access them as fast as it is possible.
10 |
11 | ## install
12 |
13 | ```
14 | npm install hyper-graph-db
15 | ```
16 |
17 | This requires node v6.x.x or greater.
18 |
19 | ## basic usage
20 |
21 | ```js
22 | var hypergraph = require('hyper-graph-db')
23 |
24 | var db = hypergraph('./my.db', { valueEncoding: 'utf-8' })
25 |
26 | var triple = { subject: 'a', predicate: 'b', object: 'c' }
27 |
28 | db.put(triple, function (err) {
29 | if (err) throw err
30 | db.get({ subject: 'a' }, function(err, list) {
31 | console.log(list)
32 | });
33 | })
34 | ```
35 |
36 | ## API
37 |
38 | #### `var db = hypergraph(storage, [key], [options])`
39 |
40 | Returns an instance of hyper-graph-db. Arguments are passed directly to hyperdb, look at its constructor [API](https://github.com/mafintosh/hyperdb#var-db--hyperdbstorage-key-options) for configuration options.
41 |
42 | Storage argument can be a Hyperdb instance, a filepath, or a storage object. For an example of storage type objects look at [dat-storage](https://github.com/datproject/dat-storage).
43 |
44 | Extra Options:
45 | ```js
46 | {
47 | index: 'hex' || 'tri', // 6 or 3 indices, default 'hex'
48 | name: string, // name that prefixes blank nodes
49 | prefixes: { // an object representing RDF namespace prefixes
50 | [sorthand]: string,
51 | },
52 | }
53 | ```
54 |
55 | The prefix option can be used to further reduce db size, as it will auto replace namespaced values with their prefered prefix.
56 |
57 | For example: `{ vcard: 'http://www.w3.org/2006/vcard/ns#' }` will store `http://www.w3.org/2006/vcard/ns#given-name` as `vcard:given-name`.
58 |
59 | **Note:** `index`, `name`, and `prefixes` can only be set when a graph db is first created. When loading an existing graph these values are also loaded from the db.
60 |
61 | #### `db.on('ready')`
62 |
63 | *This event is passed on from underlying hyperdb instance.*
64 |
65 | Emitted exactly once: when the db is fully ready and all static properties have
66 | been set. You do not need to wait for this when calling any async functions.
67 |
68 | #### `db.on('error', err)`
69 |
70 | *This event is passed on from underlying hyperdb instance.*
71 |
72 | Emitted if there was a critical error before `db` is ready.
73 |
74 | #### `db.put(triple, [callback])`
75 |
76 | Inserts **Hexastore** formated entries for triple into the graph database.
77 |
78 | #### `var stream = db.putStream(triple)`
79 |
80 | Returns a writable stream.
81 |
82 | #### `db.get(triple, [options], callback)`
83 |
84 | Returns all entries that match the triple. This allows for partial pattern-matching. For example `{ subject: 'a' })`, will return all triples with subject equal to 'a'.
85 |
86 | Options:
87 | ```js
88 | {
89 | limit: number, // limit number of triples returned
90 | offset: number, // offset returned
91 | filter: function (triple) { return bool }, // filter the results
92 | }
93 | ```
94 |
95 | #### `db.del(triple, [callback])`
96 |
97 | Remove triples indices from the graph database.
98 |
99 | #### `var stream = db.delStream(triple)`
100 |
101 | Returns a writable stream for removing entries.
102 |
103 | #### `var stream = db.getStream(triple, [options])`
104 |
105 | Returns a readable stream of all matching triples.
106 |
107 | Allowed options:
108 | ```js
109 | {
110 | limit: number, // limit number of triples returned
111 | offset: number, // offset returned
112 | filter: function (triple) { return bool }, // filter the results
113 | }
114 | ```
115 |
116 | #### `db.query(query, callback)`
117 |
118 | Allows for querying the graph with [SPARQL](https://www.w3.org/TR/sparql11-protocol/) queries.
119 | Returns all entries that match the query.
120 |
121 | SPARQL queries are implemented using [sparql-iterator](https://github.com/e-e-e/sparql-iterator) - a fork of [Linked Data Fragments Client](https://github.com/LinkedDataFragments/Client.js).
122 |
123 | #### `var stream = db.queryStream(query)`
124 |
125 | Returns a stream of results from the SPARQL query.
126 |
127 | #### `db.search(patterns, [options], callback)`
128 |
129 | Allows for Basic Graph Patterns searches where all patterns must match.
130 | Expects patterns to be an array of triple options of the form:
131 |
132 | ```js
133 | {
134 | subject: String || Variable, // required
135 | predicate: String || Variable, // required
136 | object: String || Variable, // required
137 | filter: Function, // optional
138 | }
139 | ```
140 |
141 | Allowed options:
142 | ```js
143 | {
144 | limit: number, // limit number of results returned
145 | }
146 | ```
147 |
148 | filter: function (triple) { return bool },
149 |
150 | ```js
151 | db.put([{
152 | subject: 'matteo',
153 | predicate: 'friend',
154 | object: 'daniele'
155 | }, {
156 | subject: 'daniele',
157 | predicate: 'friend',
158 | object: 'matteo'
159 | }, {
160 | subject: 'daniele',
161 | predicate: 'friend',
162 | object: 'marco'
163 | }, {
164 | subject: 'lucio',
165 | predicate: 'friend',
166 | object: 'matteo'
167 | }, {
168 | subject: 'lucio',
169 | predicate: 'friend',
170 | object: 'marco'
171 | }, {
172 | subject: 'marco',
173 | predicate: 'friend',
174 | object: 'davide'
175 | }], () => {
176 |
177 | const stream = db.search([{
178 | subject: 'matteo',
179 | predicate: 'friend',
180 | object: db.v('x')
181 | }, {
182 | subject: db.v('x'),
183 | predicate: 'friend',
184 | object: db.v('y')
185 | }, {
186 | subject: db.v('y'),
187 | predicate: 'friend',
188 | object: 'davide'
189 | }], (err, results) => {
190 | if (err) throw err
191 | console.log(results)
192 | })
193 | })
194 | ```
195 |
196 | #### `var stream = db.searchStream(queries)`
197 |
198 | Returns search results as a stream.
199 |
200 | #### `db.graphVersion()`
201 |
202 | Returns the version of hyper-graph-db that created the db.
203 |
204 | #### `db.indexType()`
205 |
206 | Returns the type of index which the graph is configured to use: `hex` or `tri`.
207 |
208 | #### `db.name()`
209 |
210 | Returns the name used for blank nodes when searching the db.
211 |
212 | #### `db.prefixes()`
213 |
214 | Returns an object representing the RDF prefixes used by the db.
215 |
216 |
--------------------------------------------------------------------------------
/test/basic.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | const expect = require('chai').expect
3 | const ram = require('random-access-memory')
4 | const tmp = require('tmp')
5 | const path = require('path')
6 | const hyperdb = require('hyperdb')
7 | const pkg = require('../package.json')
8 |
9 | const hypergraph = require('../index')
10 | const constants = require('../lib/constants')
11 | const prefixes = require('../lib/prefixes')
12 |
13 | function ramStore (filename) {
14 | // filename will be one of: data, bitfield, tree, signatures, key, secret_key
15 | // the data file will contain all your data concattenated.
16 | // just store all files in ram by returning a random-access-memory instance
17 | return ram()
18 | }
19 |
20 | describe('hypergraph', function () {
21 | let db
22 | describe('constructor storage argument', () => {
23 | context('with instance of hyperdb', () => {
24 | it('sets graph database to hyperdb passed into the constructor', () => {
25 | var hyperdbInstance = hyperdb(ramStore)
26 | db = hypergraph(hyperdbInstance)
27 | expect(db.db).to.eql(hyperdbInstance)
28 | })
29 | })
30 | })
31 | context('when newly created it adds metadata to db', () => {
32 | it('includes graph version', (done) => {
33 | db = hypergraph(ramStore)
34 | db.on('ready', () => {
35 | db.db.get('@version', (err, node) => {
36 | expect(err).to.not.be.a('error')
37 | expect(node[0].value.toString()).to.match(/\d+.\d+.\d+.*/)
38 | done()
39 | })
40 | })
41 | })
42 | it('includes index type (default)', (done) => {
43 | db = hypergraph(ramStore)
44 | db.on('ready', () => {
45 | db.db.get('@index', (err, node) => {
46 | expect(err).to.not.be.a('error')
47 | expect(node[0].value.toString()).to.eql('hex')
48 | done()
49 | })
50 | })
51 | })
52 | it('includes index type (option.index = tri)', (done) => {
53 | db = hypergraph(ramStore, { index: 'tri' })
54 | db.on('ready', () => {
55 | db.db.get('@index', (err, node) => {
56 | expect(err).to.not.be.a('error')
57 | expect(node[0].value.toString()).to.eql('tri')
58 | done()
59 | })
60 | })
61 | })
62 | it('includes name (default)', (done) => {
63 | db = hypergraph(ramStore)
64 | db.on('ready', () => {
65 | db.db.get('@name', (err, node) => {
66 | expect(err).to.not.be.a('error')
67 | expect(node[0].value.toString()).to.eql(constants.DEFAULT_BASE)
68 | done()
69 | })
70 | })
71 | })
72 | it('includes name (option.name)', (done) => {
73 | db = hypergraph(ramStore, { name: 'this://' })
74 | db.on('ready', () => {
75 | db.db.get('@name', (err, node) => {
76 | expect(err).to.not.be.a('error')
77 | expect(node[0].value.toString()).to.eql('this://')
78 | done()
79 | })
80 | })
81 | })
82 | it('includes default prefixes', (done) => {
83 | db = hypergraph(ramStore)
84 | db.on('ready', () => {
85 | const stream = db.db.createReadStream('@prefix/')
86 | var count = 0
87 | stream.on('data', (nodes) => {
88 | const prefix = prefixes.fromKey(nodes[0].key)
89 | count++
90 | expect(nodes[0].value.toString()).to.eql(constants.DEFAULT_PREFIXES[prefix])
91 | })
92 | stream.on('error', done)
93 | stream.on('end', () => {
94 | expect(count).to.eql(Object.keys(constants.DEFAULT_PREFIXES).length)
95 | done()
96 | })
97 | })
98 | })
99 | it('includes only specified prefixes (options.prefixes)', (done) => {
100 | var customPrefixes = {
101 | schema: 'http://schema.org/',
102 | library: 'http://purl.org/library/',
103 | void: 'http://rdfs.org/ns/void#',
104 | dct: 'http://purl.org/dc/terms/',
105 | rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
106 | madsrdf: 'http://www.loc.gov/mads/rdf/v1#',
107 | discovery: 'http://worldcat.org/vocab/discovery/',
108 | bgn: 'http://bibliograph.net/',
109 | pto: 'http://www.productontology.org/id/',
110 | dc: 'http://purl.org/dc/elements/1.1/'
111 | }
112 | db = hypergraph(ramStore, { prefixes: customPrefixes })
113 | db.on('ready', () => {
114 | const stream = db.db.createReadStream('@prefix/')
115 | var count = 0
116 | stream.on('data', (nodes) => {
117 | const prefix = prefixes.fromKey(nodes[0].key)
118 | count++
119 | expect(nodes[0].value.toString()).to.eql(customPrefixes[prefix])
120 | })
121 | stream.on('error', done)
122 | stream.on('end', () => {
123 | expect(count).to.eql(Object.keys(customPrefixes).length)
124 | done()
125 | })
126 | })
127 | })
128 | })
129 | context('when loading db that already exists', () => {
130 | it('does not add new metadata', (done) => {
131 | tmp.dir({ unsafeCleanup: true }, function (err, dir, cleanupCallback) {
132 | if (err) return done(err)
133 | const dbDir = path.join(dir, 'test.db')
134 | // create new hyperdb
135 | const hyper = hyperdb(dbDir)
136 | hyper.on('ready', () => {
137 | hyper.put('test', 'data', (err) => {
138 | if (err) return finish(err)
139 | openExistingDBAsGraphDB()
140 | })
141 | })
142 | hyper.on('error', finish)
143 |
144 | function openExistingDBAsGraphDB () {
145 | db = hypergraph(dbDir)
146 | db.on('ready', () => {
147 | db.db.get('@version', (err, nodes) => {
148 | expect(nodes).to.eql([])
149 | finish(err)
150 | })
151 | })
152 | db.on('error', finish)
153 | }
154 | function finish (e) {
155 | cleanupCallback()
156 | done(e)
157 | }
158 | })
159 | })
160 |
161 | context('with graph already containing index, version, name and prefixes', () => {
162 | const options = {
163 | index: 'tri',
164 | name: 'baseName',
165 | prefixes: {
166 | test: 'http://hyperreadings.info/test#',
167 | xsd: 'http://www.w3.org/2001/XMLSchema#'
168 | }
169 | }
170 | let cleanup = () => {}
171 | let graphDir
172 | before((done) => {
173 | tmp.dir({ unsafeCleanup: true }, (err, dir, cleanupCallback) => {
174 | if (err) return done(err)
175 | cleanup = cleanupCallback
176 | graphDir = dir
177 | const graph = hypergraph(graphDir, options)
178 | graph.on('ready', () => { done() })
179 | graph.on('error', done)
180 | })
181 | })
182 | after(() => {
183 | cleanup()
184 | })
185 | it('contains version that was used to create the db', (done) => {
186 | const graph = hypergraph(graphDir)
187 | graph.on('ready', () => {
188 | graph.graphVersion((err, version) => {
189 | expect(err).to.eql(null)
190 | expect(version).to.eql(pkg.version)
191 | done()
192 | })
193 | })
194 | })
195 | it('overrides options with metadata set in hyperdb (index)', (done) => {
196 | const graph = hypergraph(graphDir, { index: 'hex' })
197 | graph.on('ready', () => {
198 | graph.indexType((err, index) => {
199 | expect(err).to.eql(null)
200 | expect(index).to.eql(options.index)
201 | done()
202 | })
203 | })
204 | })
205 | it('overrides options with metadata set in hyperdb (name)', (done) => {
206 | const graph = hypergraph(graphDir, { index: 'hex', name: 'overrideMe' })
207 | graph.on('ready', () => {
208 | graph.name((err, name) => {
209 | expect(err).to.eql(null)
210 | expect(name).to.eql(options.name)
211 | done()
212 | })
213 | })
214 | })
215 | it('overrides options with metadata set in hyperdb (prefix)', (done) => {
216 | const prefixes = {
217 | thing: 'http://some.co/thing#'
218 | }
219 | const graph = hypergraph(graphDir, { index: 'hex', name: 'overrideMe', prefixes })
220 | graph.on('ready', () => {
221 | graph.prefixes((err, prefixes) => {
222 | expect(err).to.eql(null)
223 | expect(prefixes).to.deep.eql(options.prefixes)
224 | done()
225 | })
226 | })
227 | })
228 | })
229 | })
230 | })
231 |
--------------------------------------------------------------------------------
/test/data/simplefoaf.ttl:
--------------------------------------------------------------------------------
1 | @prefix foaf: .
2 |
3 | _:a foaf:givenname "Alice" .
4 | _:a foaf:family_name "Hacker" .
5 |
6 | _:b foaf:firstname "Bob" .
7 | _:b foaf:surname "Hacker" .
8 |
--------------------------------------------------------------------------------
/test/data/sparqlIn11Minutes.ttl:
--------------------------------------------------------------------------------
1 | @prefix vcard: .
2 | @prefix sn: .
3 |
4 | sn:emp1 vcard:given-name "Heidi" .
5 | sn:emp1 vcard:family-name "Smith" .
6 | sn:emp1 vcard:title "CEO" .
7 | sn:emp1 sn:hireDate "2015-01-13" .
8 | sn:emp1 sn:completedOrientation "2015-01-30" .
9 |
10 | sn:emp2 vcard:given-name "John" .
11 | sn:emp2 vcard:family-name "Smith" .
12 | sn:emp2 sn:hireDate "2015-01-28" .
13 | sn:emp2 vcard:title "Engineer" .
14 | sn:emp2 sn:completedOrientation "2015-01-30" .
15 | sn:emp2 sn:completedOrientation "2015-03-15" .
16 |
17 | sn:emp3 vcard:given-name "Francis" .
18 | sn:emp3 vcard:family-name "Jones" .
19 | sn:emp3 sn:hireDate "2015-02-13" .
20 | sn:emp3 vcard:title "Vice President" .
21 |
22 | sn:emp4 vcard:given-name "Jane" .
23 | sn:emp4 vcard:family-name "Berger" .
24 | sn:emp4 sn:hireDate "1000-03-10" .
25 | sn:emp4 vcard:title "Sales" .
26 |
--------------------------------------------------------------------------------
/test/fixture/foaf.js:
--------------------------------------------------------------------------------
1 | module.exports = [{
2 | subject: 'matteo',
3 | predicate: 'friend',
4 | object: 'daniele'
5 | }, {
6 | subject: 'daniele',
7 | predicate: 'friend',
8 | object: 'matteo'
9 | }, {
10 | subject: 'daniele',
11 | predicate: 'friend',
12 | object: 'marco'
13 | }, {
14 | subject: 'lucio',
15 | predicate: 'friend',
16 | object: 'matteo'
17 | }, {
18 | subject: 'lucio',
19 | predicate: 'friend',
20 | object: 'marco'
21 | }, {
22 | subject: 'marco',
23 | predicate: 'friend',
24 | object: 'davide'
25 | }, {
26 | subject: 'marco',
27 | predicate: 'age',
28 | object: 32
29 | }, {
30 | subject: 'daniele',
31 | predicate: 'age',
32 | object: 25
33 | }, {
34 | subject: 'lucio',
35 | predicate: 'age',
36 | object: 15
37 | }, {
38 | subject: 'davide',
39 | predicate: 'age',
40 | object: 70
41 | }]
42 |
--------------------------------------------------------------------------------
/test/fixture/homes_in_paris.js:
--------------------------------------------------------------------------------
1 | module.exports = [{
2 | subject: 'https://my-profile.eu/people/deiu/card#me',
3 | predicate: 'http://xmlns.com/foaf/0.1/name',
4 | object: '"Andrei Vlad Sambra"'
5 | }, {
6 | subject: 'http://bblfish.net/people/henry/card#me',
7 | predicate: 'http://xmlns.com/foaf/0.1/name',
8 | object: '"Henry Story"'
9 | }, {
10 | subject: 'http://presbrey.mit.edu/foaf#presbrey',
11 | predicate: 'http://xmlns.com/foaf/0.1/name',
12 | object: '"Joe Presbrey"'
13 | }, {
14 | subject: 'http://manu.sporny.org#person',
15 | predicate: 'http://xmlns.com/foaf/0.1/name',
16 | object: '"Manu Sporny"'
17 | }, {
18 | subject: 'http://melvincarvalho.com/#me',
19 | predicate: 'http://xmlns.com/foaf/0.1/name',
20 | object: '"Melvin Carvalho"'
21 | }, {
22 | subject: 'http://manu.sporny.org#person',
23 | predicate: 'http://xmlns.com/foaf/0.1/knows',
24 | object: 'http://bblfish.net/people/henry/card#me'
25 | }, {
26 | subject: 'http://presbrey.mit.edu/foaf#presbrey',
27 | predicate: 'http://xmlns.com/foaf/0.1/based_near',
28 | object: 'http://dbpedia.org/resource/Cambridge'
29 | }, {
30 | subject: 'http://melvincarvalho.com/#me',
31 | predicate: 'http://xmlns.com/foaf/0.1/based_near',
32 | object: 'http://dbpedia.org/resource/Honolulu'
33 | }, {
34 | subject: 'http://bblfish.net/people/henry/card#me',
35 | predicate: 'http://xmlns.com/foaf/0.1/based_near',
36 | object: 'http://dbpedia.org/resource/Paris'
37 | }, {
38 | subject: 'https://my-profile.eu/people/deiu/card#me',
39 | predicate: 'http://xmlns.com/foaf/0.1/based_near',
40 | object: 'http://dbpedia.org/resource/Paris'
41 | }, {
42 | subject: 'http://manu.sporny.org#person',
43 | predicate: 'http://xmlns.com/foaf/0.1/homepage',
44 | object: 'http://manu.sporny.org/'
45 | }, {
46 | subject: 'http://manu.sporny.org#person',
47 | predicate: 'http://xmlns.com/foaf/0.1/knows',
48 | object: 'http://melvincarvalho.com/#me'
49 | }, {
50 | subject: 'http://manu.sporny.org#person',
51 | predicate: 'http://xmlns.com/foaf/0.1/knows',
52 | object: 'http://presbrey.mit.edu/foaf#presbrey'
53 | }, {
54 | subject: 'http://manu.sporny.org#person',
55 | predicate: 'http://xmlns.com/foaf/0.1/knows',
56 | object: 'https://my-profile.eu/people/deiu/card#me'
57 | }]
58 |
--------------------------------------------------------------------------------
/test/join-stream.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 |
3 | const expect = require('chai').expect
4 | const ram = require('random-access-memory')
5 | const hypergraph = require('../index')
6 | const fixture = require('./fixture/foaf')
7 |
8 | function ramStore (filename) {
9 | // filename will be one of: data, bitfield, tree, signatures, key, secret_key
10 | // the data file will contain all your data concattenated.
11 | // just store all files in ram by returning a random-access-memory instance
12 | return ram()
13 | }
14 |
15 | describe('JoinStream', () => {
16 | let db
17 | beforeEach((done) => {
18 | db = hypergraph(ramStore)
19 | db.put(fixture, done)
20 | })
21 |
22 | afterEach((done) => {
23 | db.close(done)
24 | })
25 |
26 | it('should do a join with one results', (done) => {
27 | db.search([{
28 | subject: db.v('x'),
29 | predicate: 'friend',
30 | object: 'daniele'
31 | }], (err, results) => {
32 | expect(results).to.have.property('length', 1)
33 | expect(results[0]).to.have.property('x', 'matteo')
34 | done(err)
35 | })
36 | })
37 |
38 | it('should support non-array search parameter', (done) => {
39 | db.search({
40 | subject: db.v('x'),
41 | predicate: 'friend',
42 | object: 'daniele'
43 | }, (err, results) => {
44 | expect(results).to.have.property('length', 1)
45 | expect(results[0]).to.have.property('x', 'matteo')
46 | done(err)
47 | })
48 | })
49 |
50 | it('should do a join with two results', (done) => {
51 | db.search([{
52 | subject: db.v('x'),
53 | predicate: 'friend',
54 | object: 'marco'
55 | }, {
56 | subject: db.v('x'),
57 | predicate: 'friend',
58 | object: 'matteo'
59 | }], (err, results) => {
60 | expect(results).to.have.property('length', 2)
61 | expect(results[0]).to.have.property('x', 'daniele')
62 | expect(results[1]).to.have.property('x', 'lucio')
63 | done(err)
64 | })
65 | })
66 |
67 | it('should do a join with three conditions', (done) => {
68 | db.search([{
69 | subject: db.v('x'),
70 | predicate: 'friend',
71 | object: db.v('y')
72 | }, {
73 | subject: db.v('x'),
74 | predicate: 'friend',
75 | object: 'matteo'
76 | }, {
77 | subject: 'lucio',
78 | predicate: 'friend',
79 | object: db.v('y')
80 | }], (err, results) => {
81 | expect(results).to.have.property('length', 4)
82 | done(err)
83 | })
84 | })
85 |
86 | it('should return the two solutions through the searchStream interface', (done) => {
87 | var solutions = [{ x: 'daniele' }, { x: 'lucio' }]
88 | var stream = db.searchStream([{
89 | subject: db.v('x'),
90 | predicate: 'friend',
91 | object: 'marco'
92 | }, {
93 | subject: db.v('x'),
94 | predicate: 'friend',
95 | object: 'matteo'
96 | }])
97 |
98 | stream.on('data', (data) => {
99 | expect(data).to.eql(solutions.shift())
100 | })
101 |
102 | stream.on('end', done)
103 | })
104 |
105 | it('should allow to find mutual friends', (done) => {
106 | var solutions = [{ x: 'daniele', y: 'matteo' }, { x: 'matteo', y: 'daniele' }]
107 | var stream = db.searchStream([{
108 | subject: db.v('x'),
109 | predicate: 'friend',
110 | object: db.v('y')
111 | }, {
112 | subject: db.v('y'),
113 | predicate: 'friend',
114 | object: db.v('x')
115 | }])
116 |
117 | stream.on('data', (data) => {
118 | var solutionIndex = -1
119 |
120 | solutions.forEach((solution, i) => {
121 | var found = Object.keys(solutions).every((v) => {
122 | return solution[v] === data[v]
123 | })
124 | if (found) {
125 | solutionIndex = i
126 | }
127 | })
128 |
129 | if (solutionIndex !== -1) {
130 | solutions.splice(solutionIndex, 1)
131 | }
132 | })
133 |
134 | stream.on('end', () => {
135 | expect(solutions).to.have.property('length', 0)
136 | done()
137 | })
138 | })
139 |
140 | it('should allow to intersect common friends', (done) => {
141 | var solutions = [{ x: 'matteo' }, { x: 'marco' }]
142 | var stream = db.searchStream([{
143 | subject: 'lucio',
144 | predicate: 'friend',
145 | object: db.v('x')
146 | }, {
147 | subject: 'daniele',
148 | predicate: 'friend',
149 | object: db.v('x')
150 | }])
151 |
152 | stream.on('data', (data) => {
153 | expect(data).to.eql(solutions.shift())
154 | })
155 |
156 | stream.on('end', () => {
157 | expect(solutions).to.have.property('length', 0)
158 | done()
159 | })
160 | })
161 |
162 | it('should support the friend of a friend scenario', (done) => {
163 | var solutions = [{ x: 'daniele', y: 'marco' }]
164 | var stream = db.searchStream([{
165 | subject: 'matteo',
166 | predicate: 'friend',
167 | object: db.v('x')
168 | }, {
169 | subject: db.v('x'),
170 | predicate: 'friend',
171 | object: db.v('y')
172 | }, {
173 | subject: db.v('y'),
174 | predicate: 'friend',
175 | object: 'davide'
176 | }])
177 |
178 | stream.on('data', (data) => {
179 | expect(data).to.eql(solutions.shift())
180 | })
181 |
182 | stream.on('end', () => {
183 | expect(solutions).to.have.property('length', 0)
184 | done()
185 | })
186 | })
187 |
188 | xit('should return triples from a join aka materialized API', (done) => {
189 | db.search([{
190 | subject: db.v('x'),
191 | predicate: 'friend',
192 | object: 'marco'
193 | }, {
194 | subject: db.v('x'),
195 | predicate: 'friend',
196 | object: 'matteo'
197 | }], {
198 | materialized: {
199 | subject: db.v('x'),
200 | predicate: 'newpredicate',
201 | object: 'abcde'
202 | }
203 | }, (err, results) => {
204 | expect(results).to.eql([{
205 | subject: 'daniele',
206 | predicate: 'newpredicate',
207 | object: 'abcde'
208 | }, {
209 | subject: 'lucio',
210 | predicate: 'newpredicate',
211 | object: 'abcde'
212 | }])
213 | done(err)
214 | })
215 | })
216 |
217 | it('should support a friend-of-a-friend-of-a-friend scenario', (done) => {
218 | var solutions = [
219 | { x: 'daniele', y: 'matteo', z: 'daniele' },
220 | { x: 'daniele', y: 'marco', z: 'davide' }
221 | ]
222 |
223 | var stream = db.searchStream([{
224 | subject: 'matteo',
225 | predicate: 'friend',
226 | object: db.v('x')
227 | }, {
228 | subject: db.v('x'),
229 | predicate: 'friend',
230 | object: db.v('y')
231 | }, {
232 | subject: db.v('y'),
233 | predicate: 'friend',
234 | object: db.v('z')
235 | }])
236 | stream.on('data', (data) => {
237 | expect(data).to.eql(solutions.shift())
238 | })
239 |
240 | stream.on('end', () => {
241 | expect(solutions).to.have.property('length', 0)
242 | done()
243 | })
244 | })
245 |
246 | xit('should emit triples from the stream interface aka materialized API', (done) => {
247 | var triples = [{
248 | subject: 'daniele',
249 | predicate: 'newpredicate',
250 | object: 'abcde'
251 | }]
252 | var stream = db.searchStream([{
253 | subject: 'matteo',
254 | predicate: 'friend',
255 | object: db.v('x')
256 | }, {
257 | subject: db.v('x'),
258 | predicate: 'friend',
259 | object: db.v('y')
260 | }, {
261 | subject: db.v('y'),
262 | predicate: 'friend',
263 | object: 'davide'
264 | }], {
265 | materialized: {
266 | subject: db.v('x'),
267 | predicate: 'newpredicate',
268 | object: 'abcde'
269 | }
270 | })
271 |
272 | stream.on('data', (data) => {
273 | expect(data).to.eql(triples.shift())
274 | })
275 |
276 | stream.on('end', () => {
277 | expect(triples).to.have.property('length', 0)
278 | done()
279 | })
280 | })
281 |
282 | it('should support filtering inside a condition', (done) => {
283 | db.search([{
284 | subject: db.v('x'),
285 | predicate: 'friend',
286 | object: 'daniele',
287 | filter: triple => triple.subject !== 'matteo'
288 | }], (err, results) => {
289 | expect(results).to.have.length(0)
290 | done(err)
291 | })
292 | })
293 |
294 | it('should support filtering inside a second-level condition', (done) => {
295 | db.search([{
296 | subject: 'matteo',
297 | predicate: 'friend',
298 | object: db.v('y')
299 | }, {
300 | subject: db.v('y'),
301 | predicate: 'friend',
302 | object: db.v('x'),
303 | filter: (triple) => triple.object !== 'matteo'
304 | }], (err, results) => {
305 | expect(results).to.eql([{
306 | 'y': 'daniele',
307 | 'x': 'marco'
308 | }])
309 | done(err)
310 | })
311 | })
312 |
313 | xit('should support solution filtering', (done) => {
314 | db.search([{
315 | subject: 'matteo',
316 | predicate: 'friend',
317 | object: db.v('y')
318 | }, {
319 | subject: db.v('y'),
320 | predicate: 'friend',
321 | object: db.v('x')
322 | }], {
323 | filter: (context, callback) => {
324 | if (context.x !== 'matteo') {
325 | callback(null, context)
326 | } else {
327 | callback(null)
328 | }
329 | }
330 | }, (err, results) => {
331 | expect(results).to.eql([{
332 | 'y': 'daniele',
333 | 'x': 'marco'
334 | }])
335 | done(err)
336 | })
337 | })
338 |
339 | xit('should support solution filtering w/ 2 args', (done) => {
340 | // Who's a friend of matteo and aged 25.
341 | db.search([{
342 | subject: db.v('s'),
343 | predicate: 'age',
344 | object: db.v('age')
345 | }, {
346 | subject: db.v('s'),
347 | predicate: 'friend',
348 | object: 'matteo'
349 | }], {
350 | filter: (context, callback) => {
351 | if (context.age === 25) {
352 | callback(null, context) // confirm
353 | } else {
354 | callback(null) // refute
355 | }
356 | }
357 | }, (err, results) => {
358 | expect(results).to.eql([{
359 | 'age': 25,
360 | 's': 'daniele'
361 | }])
362 | done(err)
363 | })
364 | })
365 |
366 | it('should return only one solution with limit 1', (done) => {
367 | db.search([{
368 | subject: db.v('x'),
369 | predicate: 'friend',
370 | object: 'marco'
371 | }, {
372 | subject: db.v('x'),
373 | predicate: 'friend',
374 | object: 'matteo'
375 | }], { limit: 1 }, (err, results) => {
376 | expect(results).to.have.property('length', 1)
377 | expect(results[0]).to.have.property('x', 'daniele')
378 | done(err)
379 | })
380 | })
381 |
382 | it('should return only one solution with limit 1 (bis)', (done) => {
383 | db.search([{
384 | subject: 'lucio',
385 | predicate: 'friend',
386 | object: db.v('x')
387 | }, {
388 | subject: 'daniele',
389 | predicate: 'friend',
390 | object: db.v('x')
391 | }], { limit: 1 }, (err, results) => {
392 | expect(results).to.have.property('length', 1)
393 | expect(results[0]).to.have.property('x', 'matteo')
394 | done(err)
395 | })
396 | })
397 |
398 | xit('should return skip the first solution with offset 1', (done) => {
399 | db.search([{
400 | subject: db.v('x'),
401 | predicate: 'friend',
402 | object: 'marco'
403 | }, {
404 | subject: db.v('x'),
405 | predicate: 'friend',
406 | object: 'matteo'
407 | }], { offset: 1 }, (err, results) => {
408 | expect(results).to.have.property('length', 1)
409 | expect(results[0]).to.have.property('x', 'lucio')
410 | done(err)
411 | })
412 | })
413 |
414 | it('should find homes in paris', (done) => {
415 | var paris = 'http://dbpedia.org/resource/Paris'
416 | var parisians = [
417 | {
418 | webid: 'https://my-profile.eu/people/deiu/card#me',
419 | name: '"Andrei Vlad Sambra"'
420 | }, {
421 | webid: 'http://bblfish.net/people/henry/card#me',
422 | name: '"Henry Story"'
423 | }
424 | ]
425 |
426 | db.put(require('./fixture/homes_in_paris'), () => {
427 | db.search([{
428 | subject: 'http://manu.sporny.org#person',
429 | predicate: 'http://xmlns.com/foaf/0.1/knows',
430 | object: db.v('webid')
431 | }, {
432 | subject: db.v('webid'),
433 | predicate: 'http://xmlns.com/foaf/0.1/based_near',
434 | object: paris
435 | }, {
436 | subject: db.v('webid'),
437 | predicate: 'http://xmlns.com/foaf/0.1/name',
438 | object: db.v('name')
439 | }
440 | ], (err, solution) => {
441 | expect(solution).to.eql(parisians)
442 | done(err)
443 | })
444 | })
445 | })
446 | })
447 |
--------------------------------------------------------------------------------
/test/planner.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | const expect = require('chai').expect
3 | const planner = require('../lib/planner')
4 | const v = require('../lib/Variable')
5 |
6 | describe('query planner', () => {
7 | var query, expected
8 |
9 | it('should return single entries with no changes', () => {
10 | query = [ { predicate: 'friend' } ]
11 | expect(planner(query)).to.eql(query)
12 | })
13 |
14 | it('should order queries based on size', () => {
15 | query = [{
16 | subject: v('x'),
17 | predicate: 'friend',
18 | object: v('c')
19 | }, {
20 | subject: v('x'),
21 | predicate: 'abc',
22 | object: 'xyz'
23 | }]
24 |
25 | expected = [{
26 | subject: v('x'),
27 | predicate: 'abc',
28 | object: 'xyz'
29 | }, {
30 | subject: v('x'),
31 | predicate: 'friend',
32 | object: v('c')
33 | }]
34 |
35 | expect(planner(query)).to.eql(expected)
36 | })
37 |
38 | it('should return queries in the same order if they have the same variables', () => {
39 | query = [{
40 | subject: v('x'),
41 | predicate: 'friend',
42 | object: v('c')
43 | }, {
44 | subject: v('c'),
45 | predicate: 'friend',
46 | object: v('x')
47 | }]
48 |
49 | expected = [{
50 | subject: v('x'),
51 | predicate: 'friend',
52 | object: v('c')
53 | }, {
54 | subject: v('c'),
55 | predicate: 'friend',
56 | object: v('x')
57 | }]
58 |
59 | expect(planner(query)).to.eql(expected)
60 | })
61 |
62 | it('should sort three condition queries', () => {
63 | query = [{
64 | subject: v('b'),
65 | predicate: 'friend',
66 | object: v('c')
67 | }, {
68 | subject: v('a'),
69 | predicate: 'friend',
70 | object: v('b')
71 | }, {
72 | subject: 'bob',
73 | predicate: 'father',
74 | object: v('a')
75 | }]
76 |
77 | expected = [{
78 | subject: 'bob',
79 | predicate: 'father',
80 | object: v('a')
81 | }, {
82 | subject: v('a'),
83 | predicate: 'friend',
84 | object: v('b')
85 | }, {
86 | subject: v('b'),
87 | predicate: 'friend',
88 | object: v('c')
89 | }]
90 | expect(planner(query)).to.eql(expected)
91 | })
92 |
93 | it('should sort same number of vars in order of solving simplicity', () => {
94 | query = [{
95 | subject: v('x'),
96 | predicate: 'friend',
97 | object: v('c')
98 | }, {
99 | subject: v('y'),
100 | predicate: 'friend',
101 | object: v('z')
102 | }, {
103 | subject: v('c'),
104 | predicate: 'friend',
105 | object: v('y')
106 | }]
107 |
108 | expected = [
109 | query[2],
110 | query[0],
111 | query[1]
112 | ]
113 | expect(planner(query)).to.eql(expected)
114 | })
115 |
116 | it('should sort queries with same # vars based on overlapping vars (bis)', () => {
117 | query = [{
118 | subject: v('1'),
119 | predicate: 'friend',
120 | object: v('2')
121 | }, {
122 | subject: v('2'),
123 | predicate: 'friend',
124 | object: v('3')
125 | }, {
126 | subject: v('3'),
127 | predicate: 'friend',
128 | object: v('4')
129 | }, {
130 | subject: v('3'),
131 | predicate: 'has',
132 | object: v('4')
133 | }]
134 |
135 | expected = [
136 | query[3],
137 | query[2],
138 | query[1],
139 | query[0]
140 | ]
141 | expect(planner(query)).to.eql(expected)
142 | })
143 | })
144 |
145 | // it('should put the variables from the previous condition in the same order', () => {
146 | // query = [{
147 | // subject: v('x0'),
148 | // predicate: 'friend',
149 | // object: 'davide'
150 | // }, {
151 | // subject: v('x1'),
152 | // predicate: 'friend',
153 | // object: v('x0')
154 | // }, {
155 | // subject: v('x1'),
156 | // predicate: 'friend',
157 | // object: v('x2')
158 | // }]
159 |
160 | // expected = [{
161 | // subject: v('x0'),
162 | // predicate: 'friend',
163 | // object: 'davide',
164 | // stream: JoinStream,
165 | // index: 'pos'
166 | // }, {
167 | // subject: v('x1'),
168 | // predicate: 'friend',
169 | // object: v('x0'),
170 | // stream: SortJoinStream,
171 | // index: 'pos'
172 | // }, {
173 | // subject: v('x1'),
174 | // predicate: 'friend',
175 | // object: v('x2'),
176 | // stream: SortJoinStream,
177 | // index: 'pso'
178 | // }]
179 |
180 | // planner(query, (err, result) => {
181 | // expect(result).to.eql(expected)
182 | // done(err)
183 | // })
184 | // })
185 |
186 | // it('should use a SortJoinStream for another three-conditions query', (done) => {
187 | // query = [{
188 | // subject: 'matteo',
189 | // predicate: 'friend',
190 | // object: v('x')
191 | // }, {
192 | // subject: v('x'),
193 | // predicate: 'friend',
194 | // object: v('y')
195 | // }, {
196 | // subject: v('y'),
197 | // predicate: 'friend',
198 | // object: 'daniele'
199 | // }]
200 |
201 | // expected = [{
202 | // subject: 'matteo',
203 | // predicate: 'friend',
204 | // object: v('x'),
205 | // stream: JoinStream,
206 | // index: 'pso'
207 | // }, {
208 | // subject: v('x'),
209 | // predicate: 'friend',
210 | // object: v('y'),
211 | // stream: SortJoinStream,
212 | // index: 'pso'
213 | // }, {
214 | // subject: v('y'),
215 | // predicate: 'friend',
216 | // object: 'daniele',
217 | // stream: SortJoinStream,
218 | // index: 'pos'
219 | // }]
220 |
221 | // planner(query, (err, result) => {
222 | // expect(result).to.eql(expected)
223 | // done(err)
224 | // })
225 | // })
226 |
227 | // it('should use a SortJoinStream for the friend-of-a-friend-of-a-friend scenario', (done) => {
228 | // query = [{
229 | // subject: 'matteo',
230 | // predicate: 'friend',
231 | // object: v('x')
232 | // }, {
233 | // subject: v('x'),
234 | // predicate: 'friend',
235 | // object: v('y')
236 | // }, {
237 | // subject: v('y'),
238 | // predicate: 'friend',
239 | // object: v('z')
240 | // }]
241 |
242 | // expected = [{
243 | // subject: 'matteo',
244 | // predicate: 'friend',
245 | // object: v('x'),
246 | // stream: JoinStream,
247 | // index: 'pso'
248 | // }, {
249 | // subject: v('x'),
250 | // predicate: 'friend',
251 | // object: v('y'),
252 | // stream: SortJoinStream,
253 | // index: 'pso'
254 | // }, {
255 | // subject: v('y'),
256 | // predicate: 'friend',
257 | // object: v('z'),
258 | // stream: SortJoinStream,
259 | // index: 'pso'
260 | // }]
261 |
262 | // planner(query, (err, result) => {
263 | // expect(result).to.eql(expected)
264 | // done(err)
265 | // })
266 | // })
267 |
268 | // it('should pick the correct indexes with multiple predicates going out the same subject', (done) => {
269 | // query = [{
270 | // subject: v('a'),
271 | // predicate: 'friend',
272 | // object: 'marco'
273 | // }, {
274 | // subject: v('a'),
275 | // predicate: 'friend',
276 | // object: v('x1')
277 | // }, {
278 | // subject: v('x1'),
279 | // predicate: 'friend',
280 | // object: v('a')
281 | // }]
282 |
283 | // expected = [{
284 | // subject: v('a'),
285 | // predicate: 'friend',
286 | // object: 'marco',
287 | // stream: JoinStream,
288 | // index: 'pos'
289 | // }, {
290 | // subject: v('a'),
291 | // predicate: 'friend',
292 | // object: v('x1'),
293 | // stream: SortJoinStream,
294 | // index: 'pso'
295 | // }, {
296 | // subject: v('x1'),
297 | // predicate: 'friend',
298 | // object: v('a'),
299 | // stream: SortJoinStream,
300 | // index: 'pos'
301 | // }]
302 |
303 | // planner(query, (err, result) => {
304 | // expect(result).to.eql(expected)
305 | // done(err)
306 | // })
307 | // })
308 |
309 | // describe('without approximateSize', () => {
310 | // beforeEach(() => {
311 | // db = {
312 | // db: {
313 | // }
314 | // }
315 | // })
316 |
317 | // it('should order two conditions based on their size', (done) => {
318 | // query = [{
319 | // subject: 'matteo',
320 | // predicate: 'friend',
321 | // object: v('a')
322 | // }, {
323 | // subject: v('b'),
324 | // predicate: 'friend',
325 | // object: v('c')
326 | // }]
327 |
328 | // expected = [{
329 | // subject: 'matteo',
330 | // predicate: 'friend',
331 | // object: v('a'),
332 | // stream: JoinStream
333 | // }, {
334 | // subject: v('b'),
335 | // predicate: 'friend',
336 | // object: v('c'),
337 | // stream: JoinStream
338 | // }]
339 |
340 | // planner(query, (err, result) => {
341 | // expect(result).to.eql(expected)
342 | // done(err)
343 | // })
344 | // })
345 |
346 | // it('should order two conditions based on their size (bis)', (done) => {
347 | // query = [{
348 | // subject: v('b'),
349 | // predicate: 'friend',
350 | // object: v('c')
351 | // }, {
352 | // subject: 'matteo',
353 | // predicate: 'friend',
354 | // object: v('a')
355 | // }]
356 |
357 | // expected = [{
358 | // subject: 'matteo',
359 | // predicate: 'friend',
360 | // object: v('a'),
361 | // stream: JoinStream
362 | // }, {
363 | // subject: v('b'),
364 | // predicate: 'friend',
365 | // object: v('c'),
366 | // stream: JoinStream
367 | // }]
368 |
369 | // planner(query, (err, result) => {
370 | // expect(result).to.eql(expected)
371 | // done(err)
372 | // })
373 | // })
374 | // })
375 | // })
376 |
--------------------------------------------------------------------------------
/test/prefixes.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | const expect = require('chai').expect
3 | const prefixes = require('../lib/prefixes')
4 |
5 | describe('prefix utilities', () => {
6 | describe('toKey', () => {
7 | it('converts a prefix to a key', () => {
8 | expect(prefixes.toKey('this')).to.eql('@prefix/this')
9 | })
10 | })
11 |
12 | describe('fromKey', () => {
13 | it('converts a key to its prefix', () => {
14 | expect(prefixes.fromKey('@prefix/this')).to.eql('this')
15 | })
16 | })
17 |
18 | describe('fromNodes', () => {
19 | it('converts hyperdb nodes to prefix/uri object', () => {
20 | const dummyNodes = [{ key: '@prefix/this', value: Buffer.from('http://this.example.com') }]
21 | expect(prefixes.fromNodes(dummyNodes)).to.eql({ uri: 'http://this.example.com', prefix: 'this' })
22 | })
23 | it('ignores conflicts', () => {
24 | const dummyNodes = [
25 | { key: '@prefix/this', value: Buffer.from('http://this.example.com') },
26 | { key: '@prefix/this', value: Buffer.from('http://conflict.example.com') }
27 | ]
28 | expect(prefixes.fromNodes(dummyNodes)).to.eql({ uri: 'http://this.example.com', prefix: 'this' })
29 | })
30 | })
31 |
32 | describe('toPrefixed', () => {
33 | it('returns unmodified string if prefix is undefined', () => {
34 | const str = 'http://test.com/this/ok#wow'
35 | const prefixed = prefixes.toPrefixed(str)
36 | expect(prefixed).to.eql(str)
37 | })
38 | it('returns unmodified string if prefix is not set', () => {
39 | const str = 'http://test.com/this/ok#wow'
40 | const prefixed = prefixes.toPrefixed(str, { 'wow': 'http://wow.com/' })
41 | expect(prefixed).to.eql(str)
42 | })
43 | it('returns prefix string if prefix is present', () => {
44 | const str = 'http://wow.com/now/this#and_hashed'
45 | const prefixed = prefixes.toPrefixed(str, { 'wow': 'http://wow.com/' })
46 | expect(prefixed).to.eql('wow:now/this#and_hashed')
47 | })
48 | })
49 |
50 | describe('fromPrefixed', () => {
51 | it('returns unmodified string if prefix is undefined', () => {
52 | const str = 'some:thing'
53 | const prefixed = prefixes.fromPrefixed(str)
54 | expect(prefixed).to.eql(str)
55 | })
56 | it('returns unmodified string if prefix is not set', () => {
57 | const str = 'some:thing'
58 | const prefixed = prefixes.fromPrefixed(str, { 'wow': 'http://wow.com/' })
59 | expect(prefixed).to.eql(str)
60 | })
61 | it('returns prefix string if prefix is present', () => {
62 | const str = 'wow:now'
63 | const prefixed = prefixes.fromPrefixed(str, { 'wow': 'http://wow.com/' })
64 | expect(prefixed).to.eql('http://wow.com/now')
65 | })
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes1.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 |
3 | SELECT ?person
4 | WHERE
5 | {
6 | ?person vcard:family-name "Smith" .
7 | }
8 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes11.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 | PREFIX sn:
3 | PREFIX foaf:
4 | PREFIX rdf:
5 |
6 | CONSTRUCT {
7 | ?person rdf:type foaf:Person .
8 | ?person foaf:givenName ?givenName .
9 | ?person foaf:familyName ?familyName .
10 | ?person foaf:name ?fullName .
11 | }
12 | WHERE {
13 | ?person vcard:given-name ?givenName .
14 | ?person vcard:family-name ?familyName .
15 | BIND(concat(?givenName," ",?familyName) AS ?fullName)
16 | }
17 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes2.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 |
3 | SELECT ?person ?givenName
4 | WHERE
5 | {
6 | ?person vcard:family-name "Smith" .
7 | ?person vcard:given-name ?givenName .
8 | }
9 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes3.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 | PREFIX sn:
3 |
4 | SELECT ?givenName ?familyName ?hireDate
5 | WHERE
6 | {
7 | ?person vcard:given-name ?givenName .
8 | ?person vcard:family-name ?familyName .
9 | ?person sn:hireDate ?hireDate .
10 | }
11 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes4.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 | PREFIX sn:
3 |
4 | SELECT ?givenName ?familyName ?hireDate
5 | WHERE
6 | {
7 | ?person vcard:given-name ?givenName .
8 | ?person vcard:family-name ?familyName .
9 | ?person sn:hireDate ?hireDate .
10 | FILTER(?hireDate < "2015-03-01")
11 | }
12 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes5.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 | PREFIX sn:
3 |
4 | SELECT ?givenName ?familyName ?oDate
5 | WHERE
6 | {
7 | ?person vcard:given-name ?givenName .
8 | ?person vcard:family-name ?familyName .
9 | ?person sn:completedOrientation ?oDate .
10 | }
11 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes6.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 | PREFIX sn:
3 |
4 | SELECT ?givenName ?familyName ?oDate
5 | WHERE
6 | {
7 | ?person vcard:given-name ?givenName .
8 | ?person vcard:family-name ?familyName .
9 | OPTIONAL { ?person sn:completedOrientation ?oDate . }
10 | }
11 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes7.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 | PREFIX sn:
3 |
4 | SELECT ?givenName ?familyName
5 | WHERE
6 | {
7 | ?person vcard:given-name ?givenName .
8 | ?person vcard:family-name ?familyName .
9 | NOT EXISTS { ?person sn:completedOrientation ?oDate . }
10 | }
11 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes8.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 | PREFIX sn:
3 |
4 | SELECT ?givenName ?familyName ?someVariable WHERE { ?person
5 | vcard:given-name ?givenName . ?person vcard:family-name ?familyName .
6 | BIND("some value" AS ?someVariable) }
7 |
--------------------------------------------------------------------------------
/test/queries/sparqlIn11Minutes9.rq:
--------------------------------------------------------------------------------
1 | PREFIX vcard:
2 | PREFIX sn:
3 |
4 | SELECT ?givenName ?familyName ?fullName
5 | WHERE {
6 | ?person vcard:given-name ?givenName .
7 | ?person vcard:family-name ?familyName .
8 | BIND(concat(?givenName," ",?familyName) AS ?fullName)
9 | }
10 |
--------------------------------------------------------------------------------
/test/queries/union.rq:
--------------------------------------------------------------------------------
1 | PREFIX foaf:
2 | PREFIX vcard:
3 |
4 | CONSTRUCT { ?x vcard:N _:v .
5 | _:v vcard:givenName ?gname .
6 | _:v vcard:familyName ?fname }
7 | WHERE
8 | {
9 | { ?x foaf:firstname ?gname } UNION { ?x foaf:givenname ?gname } .
10 | { ?x foaf:surname ?fname } UNION { ?x foaf:family_name ?fname } .
11 | }
12 |
--------------------------------------------------------------------------------
/test/sparql.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 |
3 | var hypergraph = require('../index')
4 | var ram = require('random-access-memory')
5 | var N3 = require('n3')
6 | var fs = require('fs')
7 | var path = require('path')
8 | var chai = require('chai')
9 | var expect = chai.expect
10 |
11 | function ramStore () { return ram() }
12 |
13 | function importTurtleFile (graph, file, callback) {
14 | var parser = N3.StreamParser()
15 | var writer = graph.putStream()
16 | N3.Parser._resetBlankNodeIds()
17 | fs.createReadStream(file).pipe(parser).pipe(writer)
18 | writer.on('end', callback)
19 | writer.on('error', callback)
20 | }
21 |
22 | function testQuery (graph, queryFile, expected, done) {
23 | var query = fs.readFileSync(queryFile)
24 | var s = graph.queryStream(query.toString())
25 | s.on('data', (d) => {
26 | expect(d).to.deep.equal(expected.shift())
27 | })
28 | s.on('end', () => {
29 | expect(expected).to.have.length(0)
30 | done()
31 | })
32 | s.on('error', done)
33 | }
34 |
35 | describe('hypergraph.queryStream', () => {
36 | context('with simple foaf data', () => {
37 | var graph
38 | before((done) => {
39 | graph = hypergraph(ramStore)
40 | graph.on('ready', () => {
41 | importTurtleFile(graph, path.join(__dirname, './data/simplefoaf.ttl'), done)
42 | })
43 | })
44 |
45 | it('performs CONSTRUCT query type with UNION operator', (done) => {
46 | console.time('query')
47 | var expected = [
48 | { subject: 'hg://b0_b',
49 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#N',
50 | object: '_:b0' },
51 | { subject: '_:b0',
52 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#givenName',
53 | object: '"Bob"' },
54 | { subject: '_:b0',
55 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#familyName',
56 | object: '"Hacker"' },
57 | { subject: 'hg://b0_a',
58 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#N',
59 | object: '_:b1' },
60 | { subject: '_:b1',
61 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#givenName',
62 | object: '"Alice"' },
63 | { subject: '_:b1',
64 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#familyName',
65 | object: '"Hacker"' }
66 | ]
67 | var query = path.join(__dirname, './queries/union.rq')
68 | testQuery(graph, query, expected, done)
69 | })
70 | })
71 | describe('with data from sparql in 11 minutes', () => {
72 | var graph
73 | beforeEach((done) => {
74 | graph = hypergraph(ramStore)
75 | graph.on('ready', () => {
76 | importTurtleFile(graph, path.join(__dirname, './data/sparqlIn11Minutes.ttl'), done)
77 | })
78 | })
79 |
80 | it('executes singular query selecting singular variable', (done) => {
81 | var expected = [
82 | { '?person': 'http://www.snee.com/hr/emp1' },
83 | { '?person': 'http://www.snee.com/hr/emp2' }
84 | ]
85 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes1.rq`)
86 | testQuery(graph, query, expected, done)
87 | })
88 |
89 | it('executes two queries selecting two variables', (done) => {
90 | var expected = [
91 | { '?person': 'http://www.snee.com/hr/emp1',
92 | '?givenName': '"Heidi"' },
93 | { '?person': 'http://www.snee.com/hr/emp2',
94 | '?givenName': '"John"' }
95 | ]
96 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes2.rq`)
97 | testQuery(graph, query, expected, done)
98 | })
99 |
100 | it('executes three queries selecting three variables', (done) => {
101 | var expected = [
102 | { '?givenName': '"Heidi"',
103 | '?familyName': '"Smith"',
104 | '?hireDate': '"2015-01-13"' },
105 | { '?givenName': '"Jane"',
106 | '?familyName': '"Berger"',
107 | '?hireDate': '"1000-03-10"' },
108 | { '?givenName': '"John"',
109 | '?familyName': '"Smith"',
110 | '?hireDate': '"2015-01-28"' },
111 | { '?givenName': '"Francis"',
112 | '?familyName': '"Jones"',
113 | '?hireDate': '"2015-02-13"' }
114 | ]
115 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes3.rq`)
116 | testQuery(graph, query, expected, done)
117 | })
118 |
119 | it('executes filters based on a variable (String comparison not Date)', (done) => {
120 | var expected = [
121 | { '?givenName': '"Jane"',
122 | '?familyName': '"Berger"',
123 | '?hireDate': '"1000-03-10"' }
124 | ]
125 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes4.rq`)
126 | testQuery(graph, query, expected, done)
127 | })
128 |
129 | it('executes three queries selecting three variables (again)', (done) => {
130 | var expected = [
131 | { '?givenName': '"John"',
132 | '?familyName': '"Smith"',
133 | '?oDate': '"2015-03-15"' },
134 | { '?givenName': '"Heidi"',
135 | '?familyName': '"Smith"',
136 | '?oDate': '"2015-01-30"' },
137 | { '?givenName': '"John"',
138 | '?familyName': '"Smith"',
139 | '?oDate': '"2015-01-30"' }
140 | ]
141 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes5.rq`)
142 | testQuery(graph, query, expected, done)
143 | })
144 |
145 | it('executes three queries selecting three variables (with OPTIONAL)', (done) => {
146 | var expected = [
147 | { '?givenName': '"Jane"',
148 | '?familyName': '"Berger"',
149 | '?oDate': null },
150 | { '?givenName': '"Heidi"',
151 | '?familyName': '"Smith"',
152 | '?oDate': '"2015-01-30"' },
153 | { '?givenName': '"John"',
154 | '?familyName': '"Smith"',
155 | '?oDate': '"2015-03-15"' },
156 | { '?givenName': '"John"',
157 | '?familyName': '"Smith"',
158 | '?oDate': '"2015-01-30"' },
159 | { '?givenName': '"Francis"',
160 | '?familyName': '"Jones"',
161 | '?oDate': null }
162 | ]
163 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes6.rq`)
164 | testQuery(graph, query, expected, done)
165 | })
166 |
167 | // NOT EXISTS is not implemented in Sparql iterator
168 | xit('executes three queries selecting three variables (with NOT EXISTS)', (done) => {
169 | var expected = []
170 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes7.rq`)
171 | testQuery(graph, query, expected, done)
172 | })
173 |
174 | xit('executes two queries selecting three variables (with BIND)', (done) => {
175 | var expected = []
176 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes8.rq`)
177 | testQuery(graph, query, expected, done)
178 | })
179 |
180 | xit('executes two queries selecting three variables (with BIND and CONCAT)', (done) => {
181 | var expected = []
182 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes9.rq`)
183 | testQuery(graph, query, expected, done)
184 | })
185 |
186 | xit('executes two queries constructing new triples (with BIND and CONCAT)', (done) => {
187 | var expected = []
188 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes11.rq`)
189 | testQuery(graph, query, expected, done)
190 | })
191 | })
192 | })
193 |
--------------------------------------------------------------------------------
/test/triple-store.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | const expect = require('chai').expect
3 | const ram = require('random-access-memory')
4 | const hypergraph = require('../index')
5 |
6 | function ramStore (filename) {
7 | // filename will be one of: data, bitfield, tree, signatures, key, secret_key
8 | // the data file will contain all your data concattenated.
9 | // just store all files in ram by returning a random-access-memory instance
10 | return ram()
11 | }
12 |
13 | describe('a basic triple store', function () {
14 | let db
15 |
16 | beforeEach(function () {
17 | db = hypergraph(ramStore)
18 | })
19 |
20 | afterEach(function (done) {
21 | db.close(done)
22 | })
23 |
24 | it('should put a triple', function (done) {
25 | var triple = { subject: 'a', predicate: 'b', object: 'c' }
26 | db.put(triple, done)
27 | })
28 |
29 | describe('with a triple inserted', function () {
30 | var triple
31 |
32 | beforeEach(function (done) {
33 | triple = { subject: 'a', predicate: 'b', object: 'c' }
34 | db.put(triple, done)
35 | })
36 |
37 | it('should get it specifiying the subject', function (done) {
38 | db.get({ subject: 'a' }, (err, list) => {
39 | expect(list).to.eql([triple])
40 | done(err)
41 | })
42 | })
43 |
44 | it('should get it specifiying the object', function (done) {
45 | db.get({ object: 'c' }, (err, list) => {
46 | expect(list).to.eql([triple])
47 | done(err)
48 | })
49 | })
50 |
51 | it('should get it specifiying the predicate', function (done) {
52 | db.get({ predicate: 'b' }, (err, list) => {
53 | expect(list).to.eql([triple])
54 | done(err)
55 | })
56 | })
57 |
58 | it('should get it specifiying the subject and the predicate', function (done) {
59 | db.get({ subject: 'a', predicate: 'b' }, (err, list) => {
60 | expect(list).to.eql([triple])
61 | done(err)
62 | })
63 | })
64 |
65 | it('should get it specifiying the subject and the object', function (done) {
66 | db.get({ subject: 'a', object: 'c' }, (err, list) => {
67 | expect(list).to.eql([triple])
68 | done(err)
69 | })
70 | })
71 |
72 | it('should get it specifiying the predicate and the object', function (done) {
73 | db.get({ predicate: 'b', object: 'c' }, (err, list) => {
74 | expect(list).to.eql([triple])
75 | done(err)
76 | })
77 | })
78 |
79 | it('should get it specifiying the subject and falsy params', function (done) {
80 | db.get({ subject: 'a', predicate: false, object: null }, (err, list) => {
81 | expect(list).to.eql([triple])
82 | done(err)
83 | })
84 | });
85 |
86 | ['subject', 'predicate', 'object'].forEach(function (type) {
87 | it('should get nothing if nothing matches an only ' + type + ' query',
88 | function (done) {
89 | var query = {}
90 | query[type] = 'notfound'
91 | db.get(query, (err, list) => {
92 | expect(list).to.eql([])
93 | done(err)
94 | })
95 | })
96 | })
97 |
98 | it('should return the triple through the getStream interface', function (done) {
99 | var stream = db.getStream({ predicate: 'b' })
100 | stream.on('data', function (data) {
101 | expect(data).to.eql(triple)
102 | })
103 | stream.on('end', done)
104 | })
105 |
106 | it('should return the triple through the getStream interface with falsy params', function (done) {
107 | var stream = db.getStream({ subject: null, predicate: 'b', object: false })
108 | stream.on('data', function (data) {
109 | expect(data).to.eql(triple)
110 | })
111 | stream.on('end', done)
112 | })
113 |
114 | it('should get the triple if limit 1 is used', function (done) {
115 | db.get({}, { limit: 1 }, (err, list) => {
116 | expect(list).to.eql([triple])
117 | done(err)
118 | })
119 | })
120 |
121 | it('should get the triple if limit 0 is used', function (done) {
122 | db.get({}, { limit: 0 }, (err, list) => {
123 | expect(list).to.eql([triple])
124 | done(err)
125 | })
126 | })
127 |
128 | it('should get the triple if offset 0 is used', function (done) {
129 | db.get({}, { offset: 0 }, (err, list) => {
130 | expect(list).to.eql([triple])
131 | done(err)
132 | })
133 | })
134 |
135 | it('should not get the triple if offset 1 is used', function (done) {
136 | db.get({}, { offset: 1 }, (err, list) => {
137 | expect(list).to.eql([])
138 | done(err)
139 | })
140 | })
141 | })
142 |
143 | it('should put an array of triples', function (done) {
144 | var t1 = { subject: 'a', predicate: 'b', object: 'c' }
145 | var t2 = { subject: 'a', predicate: 'b', object: 'd' }
146 | db.put([t1, t2], done)
147 | })
148 |
149 | it('should get only triples with exact match of subjects', function (done) {
150 | var t1 = { subject: 'a1', predicate: 'b', object: 'c' }
151 | var t2 = { subject: 'a', predicate: 'b', object: 'd' }
152 | db.put([t1, t2], function () {
153 | db.get({ subject: 'a' }, function (err, matched) {
154 | expect(matched.length).to.eql(1)
155 | expect(matched[0]).to.eql(t2)
156 | done(err)
157 | })
158 | })
159 | })
160 |
161 | describe('with special characters', function () {
162 | it('should support string contain ::', function (done) {
163 | var t1 = { subject: 'a', predicate: 'b', object: 'c' }
164 | var t2 = { subject: 'a::a::a', predicate: 'b', object: 'c' }
165 | db.put([t1, t2], function () {
166 | db.get({ subject: 'a' }, (err, values) => {
167 | expect(values).to.have.lengthOf(1)
168 | done(err)
169 | })
170 | })
171 | })
172 | it('should support string contain \\::', function (done) {
173 | var t1 = { subject: 'a', predicate: 'b', object: 'c' }
174 | var t2 = { subject: 'a\\::a', predicate: 'b', object: 'c' }
175 | db.put([t1, t2], function () {
176 | db.get({ subject: 'a' }, (err, values) => {
177 | expect(values).to.have.lengthOf(1)
178 | done(err)
179 | })
180 | })
181 | })
182 | it('should support string end with :', function (done) {
183 | var t1 = { subject: 'a', predicate: 'b', object: 'c' }
184 | var t2 = { subject: 'a:', predicate: 'b', object: 'c' }
185 | db.put([t1, t2], function () {
186 | db.get({ subject: 'a:' }, (err, values) => {
187 | expect(values).to.have.lengthOf(1)
188 | expect(values[0].subject).to.equal('a:')
189 | done(err)
190 | })
191 | })
192 | })
193 | it('should support string end with \\', function (done) {
194 | var t1 = { subject: 'a', predicate: 'b', object: 'c' }
195 | var t2 = { subject: 'a\\', predicate: 'b', object: 'c' }
196 | db.put([t1, t2], function () {
197 | db.get({ subject: 'a\\' }, (err, values) => {
198 | expect(values).to.have.lengthOf(1)
199 | expect(values[0].subject).to.equal('a\\')
200 | done(err)
201 | })
202 | })
203 | })
204 | })
205 |
206 | it('should put a triple with an object to false', function (done) {
207 | var t = { subject: 'a', predicate: 'b', object: false }
208 | db.put(t, function () {
209 | // accessing underlying db instance
210 | db.db.get('spo/a/b/false', done)
211 | })
212 | })
213 |
214 | describe('with two triple inserted with the same predicate', function () {
215 | var triple1,
216 | triple2
217 |
218 | beforeEach(function (done) {
219 | triple1 = { subject: 'a1', predicate: 'b', object: 'c' }
220 | triple2 = { subject: 'a2', predicate: 'b', object: 'd' }
221 | db.put([triple1, triple2], done)
222 | })
223 |
224 | it('should get one by specifiying the subject', function (done) {
225 | db.get({ subject: 'a1' }, (err, list) => {
226 | expect(list).to.eql([triple1])
227 | done(err)
228 | })
229 | })
230 |
231 | it('should get one by specifiying the subject and a falsy predicate', function (done) {
232 | db.get({ subject: 'a1', predicate: null }, (err, list) => {
233 | expect(list).to.eql([triple1])
234 | done(err)
235 | })
236 | })
237 |
238 | it('should get two by specifiying the predicate', function (done) {
239 | db.get({ predicate: 'b' }, (err, list) => {
240 | expect(list).to.eql([triple1, triple2])
241 | done(err)
242 | })
243 | })
244 |
245 | it('should get two by specifiying the predicate and a falsy subject', function (done) {
246 | db.get({ subject: null, predicate: 'b' }, (err, list) => {
247 | expect(list).to.eql([triple1, triple2])
248 | done(err)
249 | })
250 | })
251 |
252 | it('should remove one and still return the other', function (done) {
253 | db.del(triple2, function () {
254 | db.get({ predicate: 'b' }, (err, list) => {
255 | expect(list).to.eql([triple1])
256 | done(err)
257 | })
258 | })
259 | })
260 |
261 | it('should return both triples through the getStream interface', function (done) {
262 | var triples = [triple1, triple2]
263 | var stream = db.getStream({ predicate: 'b' })
264 | stream.on('data', function (data) {
265 | expect(data).to.eql(triples.shift())
266 | })
267 |
268 | stream.on('end', done)
269 | })
270 |
271 | it('should return only one triple with limit 1', function (done) {
272 | db.get({ predicate: 'b' }, { limit: 1 }, (err, list) => {
273 | expect(list).to.eql([triple1])
274 | done(err)
275 | })
276 | })
277 |
278 | it('should return two triples with limit 2', function (done) {
279 | db.get({ predicate: 'b' }, { limit: 2 }, (err, list) => {
280 | expect(list).to.eql([triple1, triple2])
281 | done(err)
282 | })
283 | })
284 |
285 | it('should return three triples with limit 3', function (done) {
286 | db.get({ predicate: 'b' }, { limit: 3 }, (err, list) => {
287 | expect(list).to.eql([triple1, triple2])
288 | done(err)
289 | })
290 | })
291 |
292 | it('should support limit over streams', function (done) {
293 | var triples = [triple1]
294 | var stream = db.getStream({ predicate: 'b' }, { limit: 1 })
295 | stream.on('data', function (data) {
296 | expect(data).to.eql(triples.shift())
297 | })
298 |
299 | stream.on('end', done)
300 | })
301 |
302 | it('should return only one triple with offset 1', function (done) {
303 | db.get({ predicate: 'b' }, { offset: 1 }, (err, list) => {
304 | expect(list).to.eql([triple2])
305 | done(err)
306 | })
307 | })
308 |
309 | it('should return only no triples with offset 2', function (done) {
310 | db.get({ predicate: 'b' }, { offset: 2 }, (err, list) => {
311 | expect(list).to.eql([])
312 | done(err)
313 | })
314 | })
315 |
316 | it('should support offset over streams', function (done) {
317 | var triples = [triple2]
318 | var stream = db.getStream({ predicate: 'b' }, { offset: 1 })
319 | stream.on('data', function (data) {
320 | expect(data).to.eql(triples.shift())
321 | })
322 |
323 | stream.on('end', done)
324 | })
325 |
326 | xit('should return the triples in reverse order with reverse true', function (done) {
327 | db.get({ predicate: 'b', reverse: true }, (err, list) => {
328 | expect(list).to.eql([triple2, triple1])
329 | done(err)
330 | })
331 | })
332 |
333 | xit('should return the last triple with reverse true and limit 1', function (done) {
334 | db.get({ predicate: 'b', reverse: true, limit: 1 }, (err, list) => {
335 | expect(list).to.eql([triple2])
336 | done(err)
337 | })
338 | })
339 |
340 | xit('should support reverse over streams', function (done) {
341 | var triples = [triple2, triple1]
342 | var stream = db.getStream({ predicate: 'b', reverse: true })
343 | stream.on('data', function (data) {
344 | expect(data).to.eql(triples.shift())
345 | })
346 |
347 | stream.on('end', done)
348 | })
349 | })
350 |
351 | describe('with two triple inserted with the same predicate and same object', function () {
352 | var triple1
353 | var triple2
354 |
355 | beforeEach(function (done) {
356 | triple1 = { subject: 'a', predicate: 'b', object: 'c' }
357 | triple2 = { subject: 'a2', predicate: 'b', object: 'c' }
358 | db.put([triple1, triple2], done)
359 | })
360 |
361 | it('should get one by specifiying the subject', function (done) {
362 | db.get({ subject: 'a' }, (err, list) => {
363 | expect(list).to.eql([triple1])
364 | done(err)
365 | })
366 | })
367 |
368 | it('should get one by specifiying the exact triple', function (done) {
369 | db.get({ subject: 'a', predicate: 'b', object: 'c' }, (err, list) => {
370 | expect(list).to.eql([triple1])
371 | done(err)
372 | })
373 | })
374 |
375 | it('should get one by specifiying the subject and a falsy predicate', function (done) {
376 | db.get({ subject: 'a', predicate: null }, (err, list) => {
377 | expect(list).to.eql([triple1])
378 | done(err)
379 | })
380 | })
381 |
382 | it('should get two by specifiying the predicate', function (done) {
383 | db.get({ predicate: 'b' }, (err, list) => {
384 | expect(list).to.eql([triple1, triple2])
385 | done(err)
386 | })
387 | })
388 |
389 | it('should get two by specifiying the predicate and a falsy subject', function (done) {
390 | db.get({ subject: null, predicate: 'b' }, (err, list) => {
391 | expect(list).to.eql([triple1, triple2])
392 | done(err)
393 | })
394 | })
395 |
396 | it('should remove one and still return the other', function (done) {
397 | db.del(triple2, function () {
398 | db.get({ predicate: 'b' }, (err, list) => {
399 | expect(list).to.eql([triple1])
400 | done(err)
401 | })
402 | })
403 | })
404 |
405 | it('should return both triples through the getStream interface', function (done) {
406 | var triples = [triple1, triple2]
407 | var stream = db.getStream({ predicate: 'b' })
408 | stream.on('data', function (data) {
409 | expect(data).to.eql(triples.shift())
410 | })
411 |
412 | stream.on('end', done)
413 | })
414 |
415 | it('should return only one triple with limit 1', function (done) {
416 | db.get({ predicate: 'b' }, { limit: 1 }, (err, list) => {
417 | expect(list).to.eql([triple1])
418 | done(err)
419 | })
420 | })
421 |
422 | it('should return two triples with limit 2', function (done) {
423 | db.get({ predicate: 'b' }, { limit: 2 }, (err, list) => {
424 | expect(list).to.eql([triple1, triple2])
425 | done(err)
426 | })
427 | })
428 |
429 | it('should return three triples with limit 3', function (done) {
430 | db.get({ predicate: 'b' }, { limit: 3 }, (err, list) => {
431 | expect(list).to.eql([triple1, triple2])
432 | done(err)
433 | })
434 | })
435 |
436 | it('should support limit over streams', function (done) {
437 | var triples = [triple1]
438 | var stream = db.getStream({ predicate: 'b' }, { limit: 1 })
439 | stream.on('data', function (data) {
440 | expect(data).to.eql(triples.shift())
441 | })
442 |
443 | stream.on('end', done)
444 | })
445 |
446 | it('should return only one triple with offset 1', function (done) {
447 | db.get({ predicate: 'b' }, { offset: 1 }, (err, list) => {
448 | expect(list).to.eql([triple2])
449 | done(err)
450 | })
451 | })
452 |
453 | it('should return only no triples with offset 2', function (done) {
454 | db.get({ predicate: 'b' }, { offset: 2 }, (err, list) => {
455 | expect(list).to.eql([])
456 | done(err)
457 | })
458 | })
459 |
460 | it('should support offset over streams', function (done) {
461 | var triples = [triple2]
462 | var stream = db.getStream({ predicate: 'b' }, { offset: 1 })
463 | stream.on('data', function (data) {
464 | expect(data).to.eql(triples.shift())
465 | })
466 |
467 | stream.on('end', done)
468 | })
469 |
470 | xit('should return the triples in reverse order with reverse true', function (done) {
471 | db.get({ predicate: 'b', reverse: true }, (err, list) => {
472 | expect(list).to.eql([triple2, triple1])
473 | done(err)
474 | })
475 | })
476 |
477 | xit('should return the last triple with reverse true and limit 1', function (done) {
478 | db.get({ predicate: 'b', reverse: true, limit: 1 }, (err, list) => {
479 | expect(list).to.eql([triple2])
480 | done(err)
481 | })
482 | })
483 |
484 | xit('should support reverse over streams', function (done) {
485 | var triples = [triple2, triple1]
486 | var stream = db.getStream({ predicate: 'b', reverse: true })
487 |
488 | stream.on('data', function (data) {
489 | expect(data).to.eql(triples.shift())
490 | })
491 |
492 | stream.on('end', done)
493 | })
494 | })
495 |
496 | xdescribe('with 10 triples inserted', function () {
497 | beforeEach(function (done) {
498 | var triples = []
499 | for (var i = 0; i < 10; i++) {
500 | triples[i] = { subject: 's', predicate: 'p', object: 'o' + i }
501 | }
502 | db.put(triples, done)
503 | })
504 |
505 | if (!process.browser) {
506 | it('should return the approximate size', function (done) {
507 | db.approximateSize({ predicate: 'b' }, function (err, size) {
508 | expect(size).to.be.a('number')
509 | done(err)
510 | })
511 | })
512 | }
513 | })
514 |
515 | it('should put triples using a stream', function (done) {
516 | var t1 = { subject: 'a', predicate: 'b', object: 'c' }
517 | var t2 = { subject: 'a', predicate: 'b', object: 'd' }
518 | var stream = db.putStream()
519 | stream.on('end', done)
520 |
521 | stream.write(t1)
522 | stream.end(t2)
523 | })
524 |
525 | it('should store the triples written using a stream', function (done) {
526 | var t1 = { subject: 'a', predicate: 'b', object: 'c' }
527 | var t2 = { subject: 'a', predicate: 'b', object: 'd' }
528 | var stream = db.putStream()
529 |
530 | stream.write(t1)
531 | stream.end(t2)
532 |
533 | stream.on('end', function () {
534 | var triples = [t1, t2]
535 | var readStream = db.getStream({ predicate: 'b' })
536 |
537 | readStream.on('data', function (data) {
538 | expect(data).to.eql(triples.shift())
539 | })
540 |
541 | readStream.on('end', done)
542 | })
543 | })
544 |
545 | it('should del the triples using a stream', function (done) {
546 | var t1 = { subject: 'a', predicate: 'b', object: 'c' }
547 | var t2 = { subject: 'a', predicate: 'b', object: 'd' }
548 | var stream = db.putStream()
549 |
550 | stream.write(t1)
551 | stream.end(t2)
552 |
553 | stream.on('end', function () {
554 | var delStream = db.delStream()
555 | delStream.write(t1)
556 | delStream.end(t2)
557 |
558 | delStream.on('end', function () {
559 | var readStream = db.getStream({ predicate: 'b' })
560 |
561 | var results = []
562 | readStream.on('data', function (data) {
563 | results.push(data)
564 | })
565 |
566 | readStream.on('end', function () {
567 | expect(results).to.have.property('length', 0)
568 | done()
569 | })
570 | })
571 | })
572 | })
573 |
574 | it('should support filtering', function (done) {
575 | var triple1 = { subject: 'a', predicate: 'b', object: 'd' }
576 | var triple2 = { subject: 'a', predicate: 'b', object: 'c' }
577 |
578 | db.put([triple1, triple2], function () {
579 | function filter (triple) {
580 | return triple.object === 'd'
581 | }
582 |
583 | db.get({ subject: 'a', predicate: 'b' }, { filter: filter }, (err, results) => {
584 | expect(results).to.eql([triple1])
585 | done(err)
586 | })
587 | })
588 | })
589 | })
590 |
591 | describe('deferred open support', function () {
592 | var db
593 |
594 | afterEach(function (done) {
595 | db.close(done)
596 | })
597 |
598 | it('should support deferred search', function (done) {
599 | db = hypergraph(ramStore)
600 | db.search([{ predicate: 'likes' }], function () {
601 | done()
602 | })
603 | })
604 | })
605 |
606 | describe('generateBatch', function () {
607 | var db
608 | context('without index option', () => {
609 | beforeEach(function () {
610 | db = hypergraph(ramStore)
611 | })
612 |
613 | afterEach(function (done) {
614 | db.close(done)
615 | })
616 | it('should generate a batch from a triple with length 6', function () {
617 | var triple = { subject: 'a', predicate: 'b', object: 'c', extra: 'data' }
618 | var ops = db._generateBatch(triple)
619 | expect(ops).to.have.property('length', 6)
620 | ops.forEach(function (op) {
621 | expect(op).to.have.property('type', 'put')
622 | expect(JSON.parse(op.value)).to.eql({ extra: 'data' })
623 | })
624 | })
625 |
626 | it('should generate a batch of type', function () {
627 | var triple = { subject: 'a', predicate: 'b', object: 'c' }
628 | var ops = db._generateBatch(triple, 'del')
629 | expect(ops).to.have.property('length', 6)
630 | ops.forEach(function (op) {
631 | expect(op).to.have.property('type', 'del')
632 | expect(JSON.parse(op.value)).to.eql(null)
633 | })
634 | })
635 | })
636 | context('with index option set to small', () => {
637 | beforeEach(function () {
638 | db = hypergraph(ramStore, null, { index: 'tri' })
639 | })
640 |
641 | afterEach(function (done) {
642 | db.close(done)
643 | })
644 | it('should generate a batch from a triple with length 3', function () {
645 | var triple = { subject: 'a', predicate: 'b', object: 'c', other: 'stuff' }
646 | var ops = db._generateBatch(triple)
647 | expect(ops).to.have.property('length', 3)
648 | ops.forEach(function (op) {
649 | expect(op).to.have.property('type', 'put')
650 | expect(JSON.parse(op.value)).to.eql({ other: 'stuff' })
651 | })
652 | })
653 |
654 | it('should generate a batch of type', function () {
655 | var triple = { subject: 'a', predicate: 'b', object: 'c', other: 'stuff' }
656 | var ops = db._generateBatch(triple, 'del')
657 | expect(ops).to.have.property('length', 3)
658 | ops.forEach(function (op) {
659 | expect(op).to.have.property('type', 'del')
660 | expect(JSON.parse(op.value)).to.eql(null)
661 | })
662 | })
663 | })
664 | })
665 |
--------------------------------------------------------------------------------
/test/utils.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | const expect = require('chai').expect
3 | const utils = require('../lib/utils')
4 |
5 | describe('util functions', () => {
6 | describe('encodeTriple', () => {
7 | it('does nothing if escape is not needed', () => {
8 | var encoded = utils.encodeTriple({ subject: 'a-subject', object: 'a-object', predicate: 'a-predicate', data: 'some data' })
9 | expect(encoded).to.eql({
10 | subject: 'a-subject',
11 | object: 'a-object',
12 | predicate: 'a-predicate',
13 | data: 'some data'
14 | })
15 | })
16 | it('escapes all forward slashes', () => {
17 | var encoded = utils.encodeTriple({ subject: 'a/subject', object: 'a-object', predicate: 'a/predicate', data: 'some/data' })
18 | expect(encoded).to.eql({
19 | subject: 'a%2Fsubject',
20 | object: 'a-object',
21 | predicate: 'a%2Fpredicate',
22 | data: 'some/data'
23 | })
24 | })
25 | it('escapes partial triples', () => {
26 | var encoded = utils.encodeTriple({ predicate: 'a/predicate', data: 'some/data' })
27 | expect(encoded).to.eql({
28 | predicate: 'a%2Fpredicate',
29 | data: 'some/data'
30 | })
31 | })
32 | context('with prefixes passed as argument', () => {
33 | it('addes known prefixes', () => {
34 | var triple = { subject: 'http://nothing.info/forever#maybe', object: 'http://everything.org/tomorrow', data: 'some/data' }
35 | var encoded = utils.encodeTriple(triple, {
36 | nothing: 'http://nothing.info/forever',
37 | inf: 'http://everything.org/'
38 | })
39 | expect(encoded).to.eql({
40 | subject: 'nothing:#maybe',
41 | object: 'inf:tomorrow',
42 | data: 'some/data'
43 | })
44 | })
45 | })
46 | })
47 |
48 | describe('encodeKey', () => {
49 | it('generates a unique index key for a triple (spo)', () => {
50 | var key = utils.encodeKey('spo', { subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' })
51 | expect(key).to.eql('spo/a-subject/a-predicate/a-object')
52 | })
53 | it('generates a unique index key for a triple (spo)', () => {
54 | var key = utils.encodeKey('sop', { subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' })
55 | expect(key).to.eql('sop/a-subject/a-object/a-predicate')
56 | })
57 | it('generates a unique index key for a triple (osp)', () => {
58 | var key = utils.encodeKey('osp', { subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' })
59 | expect(key).to.eql('osp/a-object/a-subject/a-predicate')
60 | })
61 |
62 | it('escapes forward slashes in the triple', () => {
63 | var key = utils.encodeKey('spo', { subject: 'a%2Fsubject' })
64 | expect(key).to.eql('spo/a%2Fsubject/')
65 | })
66 | })
67 |
68 | describe('decodeKey', () => {
69 | it('generates a triple from a index key (spo)', () => {
70 | var triple = utils.decodeKey('spo/a-subject/a-predicate/a-object')
71 | expect(triple).to.eql({ subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' })
72 | })
73 | it('generates a triple from a index key (spo)', () => {
74 | var triple = utils.decodeKey('sop/a-subject/a-object/a-predicate')
75 | expect(triple).to.eql({ subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' })
76 | })
77 | it('generates a triple from a index key (osp)', () => {
78 | var triple = utils.decodeKey('osp/a-object/a-subject/a-predicate')
79 | expect(triple).to.eql({ subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' })
80 | })
81 |
82 | it('unescapes escaped /‘s from index key (spo)', () => {
83 | var triple = utils.decodeKey('spo/a%2Fsubject/a%2Fpredicate/a%2Fobject')
84 | expect(triple).to.eql({ subject: 'a/subject', object: 'a/object', predicate: 'a/predicate' })
85 | })
86 | })
87 | })
88 |
--------------------------------------------------------------------------------
/test/variable.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | const expect = require('chai').expect
3 | var Variable = require('../lib/Variable')
4 |
5 | describe('Variable', () => {
6 | it('should have a name', () => {
7 | var v = new Variable('x')
8 | expect(v).to.have.property('name', 'x')
9 | })
10 |
11 | it('should have a name (bis)', () => {
12 | var v = new Variable('y')
13 | expect(v).to.have.property('name', 'y')
14 | })
15 |
16 | describe('#isBound', () => {
17 | var instance
18 |
19 | beforeEach(() => {
20 | instance = new Variable('x')
21 | })
22 |
23 | it('should return true if there is a key in the solution', () => {
24 | expect(instance.isBound({ x: 'hello' })).to.equal(true)
25 | })
26 |
27 | it('should return false if there is no key in the solution', () => {
28 | expect(instance.isBound({})).to.equal(false)
29 | })
30 |
31 | it('should return false if there is another key in the solution', () => {
32 | expect(instance.isBound({ hello: 'world' })).to.equal(false)
33 | })
34 | })
35 |
36 | describe('#bind', () => {
37 | var instance
38 |
39 | beforeEach(() => {
40 | instance = new Variable('x')
41 | })
42 |
43 | it('should return a different object', () => {
44 | var solution = {}
45 | expect(instance.bind(solution, 'hello')).to.not.be.equal(solution)
46 | })
47 |
48 | it('should set an element in the solution', () => {
49 | var solution = {}
50 | expect(instance.bind(solution, 'hello')).to.be.deep.equal({ x: 'hello' })
51 | })
52 |
53 | it('should copy values', () => {
54 | var solution = { y: 'world' }
55 | expect(instance.bind(solution, 'hello')).to.be.deep.equal({ x: 'hello', y: 'world' })
56 | })
57 | })
58 |
59 | describe('#isBindable', () => {
60 | var instance
61 |
62 | beforeEach(() => {
63 | instance = new Variable('x')
64 | })
65 |
66 | it('should bind to the same value', () => {
67 | expect(instance.isBindable({ x: 'hello' }, 'hello')).to.equal(true)
68 | })
69 |
70 | it('should not bind to a different value', () => {
71 | expect(instance.isBindable({ x: 'hello' }, 'hello2')).to.equal(false)
72 | })
73 |
74 | it('should bind if the key is not present', () => {
75 | expect(instance.isBindable({}, 'hello')).to.equal(true)
76 | })
77 | })
78 | })
79 |
--------------------------------------------------------------------------------