hyper-readings
Version:
a tool for making and sharing p2p distributed reading lists
292 lines (255 loc) • 10.2 kB
JavaScript
import events from 'events'
import fs from 'fs'
import hypergraph from 'hyper-graph-db'
import inherits from 'inherits'
import uuid from 'uuid/v4'
import importers from './importers/index'
import createNodeInterface from './definitions/index'
import { PREFIXES } from './constants'
import { spo, ops, toRdfValue } from './utils'
function resolveCallback (resolve, reject) {
return function (err, result) {
if (err) return reject(err)
resolve(result)
}
}
function HyperReadings (storage, key, opts) {
if (!(this instanceof HyperReadings)) return new HyperReadings(storage, key, opts)
if (typeof key !== 'string' && !opts) {
opts = key
key = null
}
events.EventEmitter.call(this)
opts = opts || {}
if (!opts.prefixes) opts.prefixes = PREFIXES
if (!opts.name) opts.name = 'hr://'
this.swarm = opts.swarm
this.network = null
this._nodeCount = 0
this.graph = hypergraph(storage, key, opts)
this.graph.on('ready', (e) => {
this.emit('ready', e)
})
}
inherits(HyperReadings, events.EventEmitter)
/** Get underlying hyperdb key */
HyperReadings.prototype.key = function () {
return this.graph.db.key.toString('hex')
}
HyperReadings.prototype.joinNetwork = function (opts) {
if (!this.swarm) {
console.warn('Cannot join network - no swarm set.')
return
}
if (this.network) {
console.warn('Already joined network')
return this.network
}
this.network = this.swarm(this.graph.db, opts)
return this.network
}
HyperReadings.prototype.leaveNetwork = async function () {
if (!this.network) return
return new Promise((resolve, reject) => {
this.network.leave(this.graph.db.discoveryKey)
this.network.destroy(resolveCallback(resolve, reject))
}).then(() => { this.network = null })
}
HyperReadings.prototype.importZoteroReference = function (data) {
return this.import(data, { type: 'zoteroApi' })
}
HyperReadings.prototype.importFile = function (filename, opts) {
if (!filename || typeof filename !== 'string') throw new Error('HyperReadings.importFile requires filename to be a string')
// infer import type from filename
let type = opts && opts.type
if (!type) {
const match = filename.match(/\.(w+)$/)
if ((!match || importers[match[1]])) throw new Error(`HyperReadings.import does not recognize file extension`)
type = match[1]
} else if (!importers[type]) {
throw new Error(`HyperReadings.import does not recognize type ${type}`)
}
return new Promise((resolve, reject) => fs.readFile(filename, resolveCallback))
.then(data => this.import(data, type))
}
HyperReadings.prototype.import = function (data, opts) {
if (!data) return Promise.resolve()
if (!opts || !opts.type) throw new Error('HyperReadings.import requires opts.type to be set')
if (!importers[opts.type]) throw new Error(`HyperReadings.import does not recognize type ${opts.type}`)
return importers[opts.type](this, data)
}
HyperReadings.prototype.setTitle = function (title) {
if (!title || typeof title !== 'string') throw new Error('Title must be a string')
return new Promise((resolve, reject) => this.graph.db.put('@title', title, (e) => {
if (e) return reject(e)
resolve()
}))
}
HyperReadings.prototype.title = function () {
return new Promise((resolve, reject) => this.graph.db.get('@title', (e, nodes) => {
if (e) return reject(e)
resolve(nodes)
})).then((nodes) => {
if (!nodes || nodes.length === 0) {
return 'untitled'
}
return nodes[0].value.toString()
})
}
/** Get hyperreading root node */
HyperReadings.prototype.root = async function () {
return this.nodeByType('hr:root')
}
HyperReadings.prototype.head = async function () {
return this.nodeByType('hr:head')
}
HyperReadings.prototype.body = async function () {
return this.nodeByType('hr:body')
}
HyperReadings.prototype.iterate = async function (...args) {
const r = await this.root()
return r.iterate(...args)
}
HyperReadings.prototype.createCollection = async function (name, description) {
// TODO: check if collection with name already exists
return this.createNode('as:Collection', { 'as:name': name, 'as:content': description })
}
HyperReadings.prototype.collections = async function () {
return this.nodesByType('as:Collection')
}
HyperReadings.prototype.getCollection = async function (name) {
const v = this.graph.v
const query = [ spo(v('id'), 'rdf:type', 'as:Collection') ]
if (name) query.push(spo(v('id'), 'as:name', toRdfValue(name)))
const collections = await this._search(query, { limit: 1 })
if (collections.length === 0) return null
return this.node({ name: collections[0].id, type: 'as:Collection' })
}
HyperReadings.prototype.exists = async function (id, type) {
const node = await this._get(spo(id, type ? 'rdf:type' : undefined, type), { limit: 1 })
return !!(node && node.length > 0)
}
HyperReadings.prototype.findPeople = async function (opts) {
const v = this.graph.v
const query = [ spo(v('id'), 'rdf:type', 'bf:Person') ]
if (opts && opts.name) query.push(spo(v('id'), 'foaf:name', toRdfValue(opts.name)))
if (opts && opts.firstName) query.push(spo(v('id'), 'foaf:firstName', toRdfValue(opts.firstName)))
if (opts && opts.lastName) query.push(spo(v('id'), 'foaf:lastName', toRdfValue(opts.lastName)))
const people = await this._search(query)
return Promise.all(people.map(p => this.node({ name: p.id, type: 'bf:Person' })))
}
HyperReadings.prototype.findAgents = async function (opts) {
const v = this.graph.v
const query = [ spo(v('id'), 'rdf:type', 'bf:Agent') ]
if (opts && opts.label) query.push(spo(v('id'), 'rdfs:label', opts.label))
const agents = await this._search(query)
return Promise.all(agents.map(p => this.node({ name: p.id, type: 'bf:Agent' })))
}
HyperReadings.prototype.findPlaces = async function (opts) {
const v = this.graph.v
const query = [ spo(v('id'), 'rdf:type', 'bf:Place') ]
if (opts && opts.label) query.push(spo(v('id'), 'rdfs:label', opts.label))
const places = await this._search(query)
return Promise.all(places.map(p => this.node({ name: p.id, type: 'bf:Place' })))
}
HyperReadings.prototype.findSeriesWithTitle = async function (title) {
// todo: Not sure if this is the most efficient way to get a series
const found = await this._query(`
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX bf: <http://id.loc.gov/ontologies/bibframe/>
select * where {
{
?title rdf:type bf:AbbreviatedTitle .
} UNION {
?title rdf:type bf:Title .
}
?title rdfs:label ${toRdfValue(title)} .
?id rdf:type bf:Instance .
?id bf:title ?title .
}`)
return Promise.all(found.map(p => this.node({ name: p['?id'], type: 'bf:Instance' })))
}
/** Get all nodes by type */
HyperReadings.prototype.nodesByType = async function (type, opts) {
// big question - how do we maintain new node names not clashing with old.
const triples = await this._get(ops(type, 'rdf:type'), opts)
return Promise.all(triples.map(triple => this.node(triple)))
}
/** Get all nodes by type */
HyperReadings.prototype.nodeByType = async function (type, opts) {
// big question - how do we maintain new node names not clashing with old.
const nodes = await this.nodesByType(type, { limit: 1 })
return nodes.length > 0 ? nodes[0] : null
}
/** Cast to specific node type */
HyperReadings.prototype.node = async function (data, context) {
let name
let type
if (typeof data === 'string') { // from named node
name = data
} else if (data.subject) { // from triple
name = data.subject
if (data.predicate === 'rdf:type' && data.object) type = data.object
} else if (data.name) { // from other node like object
name = data.name
type = data.type
}
if (!type) {
type = await this._getType(name)
}
return createNodeInterface(this, name, type, context)
}
/** Create a new blank node of type */
HyperReadings.prototype.createNode = async function (type, properties) {
if (!type) {
throw new Error('Cannot create a node without type')
}
let nodeId = properties && properties.id
if (!nodeId) {
// big question - how do we maintain new node names not clashing with old.
// const name = await this._name()
nodeId = await this.newBlankNodeName()
}
const triple = spo(nodeId, 'rdf:type', type)
await this._put(triple)
// BUG triple is mutated by put, change this once fixed
const node = await this.node(spo(nodeId, 'rdf:type', type))
if (properties) {
// TODO: enable arrays to be added too.
await Promise.all(Object.keys(properties).map((key) => {
if (key === 'id' || typeof properties[key] === 'undefined') return
return node.set(key, properties[key])
}))
}
return node
}
HyperReadings.prototype.newBlankNodeName = async function () {
const name = await this._name()
// should probably use hashes so as to avoid collisions
return name + 'n' + uuid()
}
/** Returns node type */
HyperReadings.prototype._getType = async function (subject) {
var triples = await this._get(spo(subject, 'rdf:type'), { limit: 1 })
return triples.length ? triples[0].object : undefined
}
HyperReadings.prototype._name = function (triple, opts) {
return new Promise((resolve, reject) => this.graph.name(resolveCallback(resolve, reject)))
}
HyperReadings.prototype._get = function (triple, opts) {
return new Promise((resolve, reject) => this.graph.get(triple, opts, resolveCallback(resolve, reject)))
}
HyperReadings.prototype._put = function (triple) {
return new Promise((resolve, reject) => this.graph.put(triple, resolveCallback(resolve, reject)))
}
HyperReadings.prototype._del = function (triple) {
return new Promise((resolve, reject) => this.graph.del(triple, resolveCallback(resolve, reject)))
}
HyperReadings.prototype._search = function (patterns, opts) {
return new Promise((resolve, reject) => this.graph.search(patterns, opts, resolveCallback(resolve, reject)))
}
HyperReadings.prototype._query = function (query) {
return new Promise((resolve, reject) => this.graph.query(query, resolveCallback(resolve, reject)))
}
export default HyperReadings