hyper-readings
Version:
a tool for making and sharing p2p distributed reading lists
245 lines (222 loc) • 8.17 kB
JavaScript
import {
spo,
ops,
isRdfLiteral,
fromRdfValue,
toRdfValue,
isNode
} from '../utils'
const StandardBehavior = {}
// STANDARD FUNCTIONALITY
StandardBehavior._castToNodeOrValue = async function (value) {
if (isRdfLiteral(value)) {
return fromRdfValue(value)
}
return this.hr.node(value)
}
StandardBehavior.get = async function (attr, value) {
const nodes = await this.hr._get(spo(this.name, attr, value), { limit: 1 })
// console.log('get', spo(this.name, attr, value), nodes)
if (!nodes || nodes.length === 0) return null
return this._castToNodeOrValue(nodes[0].object)
}
StandardBehavior.all = async function (attr, value) {
const triples = await this.hr._get(spo(this.name, attr, value))
return Promise.all(triples.map(node => this._castToNodeOrValue(node.object)))
}
StandardBehavior.add = function (attr, value) {
return this.set(attr, value, true)
}
StandardBehavior.set = async function (attr, value, allowMultiple) {
if (!attr) throw new Error('node.set() requires attribute')
if (typeof attr !== 'string') {
// TODO: set multiple
}
// 1. delete all `< node name ? >` triples
if (!allowMultiple) {
// replace with stream
// const triples = await this.hr._get(spo(this.name, attr))
// if object is a node this will potentially leave floating nodes.
// so we call destroy if the node has more than one parent
const oldValues = await this.all(attr)
for (var i = 0; i < oldValues.length; i++) {
const v = oldValues[i]
if (isNode(v)) {
const parents = await v.parents(null, { limit: 2 })
if (parents.length === 1) {
await v.destroy()
} else {
await this.hr._del(spo(this.name, attr, v.name))
}
} else {
await this.remove(attr, v)
}
}
// await Promise.all(triples.map(triple => this.hr._del(triple)))
}
// 2. create new key `< node name value >`
await this.hr._put(spo(this.name, attr, toRdfValue(value)))
// 3. return node
return this
}
function equalityCheck (a, b) {
if (typeof a === 'object' && typeof b === 'object') {
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (!equalityCheck(a, b)) return false
}
return true
}
return a.name === b.name
}
return a === b
}
function deepIncludes (array, value) {
return !!array.find(v => equalityCheck(v, value))
}
function shouldDeleteForUpdate (newPredicates, v, properties, currentProps) {
return !newPredicates.includes(v) || !equalityCheck(properties[v], currentProps[v])
}
function shouldDeleteForMerge (newPredicates, v, properties, currentProps) {
return (properties[v] && !equalityCheck(properties[v], currentProps[v]))
}
function _update (shouldDelete) {
return async function (properties) {
// get all existing properties
const currentProps = await this.properties()
const oldPredicates = Object.keys(currentProps)
const newPredicates = Object.keys(properties)
const del = oldPredicates.reduce((p, v) => {
if (v === 'rdf:type') return p
if (shouldDelete(newPredicates, v, properties, currentProps)) p.push(v)
return p
}, [])
await Promise.all(del.map(v => {
const value = currentProps[v]
if (Array.isArray(value)) {
const newValueIsAnArray = Array.isArray(properties[v])
return Promise.all(value.map(val => {
if (newValueIsAnArray && deepIncludes(properties[v], val)) return
return this.remove(v, val)
}))
}
return this.remove(v, value)
}))
await Promise.all(newPredicates.map(v => {
const value = properties[v]
const oldValue = currentProps[v]
if (equalityCheck(value, oldValue)) return
if (Array.isArray(value)) {
const oldValueIsArray = Array.isArray(oldValue)
return Promise.all(value.map(val => {
if (oldValueIsArray && deepIncludes(oldValue, val)) return
return this.add(v, val)
}))
}
return this.set(v, value)
}))
}
}
StandardBehavior.update = _update(shouldDeleteForUpdate)
StandardBehavior.merge = _update(shouldDeleteForMerge)
StandardBehavior.has = async function (attr, value) {
var nodes = await this.hr._get(spo(this.name, attr, value && toRdfValue(value)), { limit: 1 })
if (!nodes) return false
if (nodes.length === 0) return false
return true
}
StandardBehavior.remove = async function (attr, value) {
// get the nodes
// if this is a parent we already know that these exist so should not check.
// console.log('remove', this.name, attr, value && value.name)
// console.log(spo(this.name, attr, value && toRdfValue(value)))
const triples = await this.hr._get(spo(this.name, attr, value && toRdfValue(value)))
if (!triples) return this
await Promise.all(triples.map(triple => this.hr._del(triple)))
return this
}
/** return array of parents elements */
StandardBehavior.parents = async function (relation, opts) {
const triples = await this.hr._get(ops(this.name, relation), opts)
// console.log('parents', triples.map(t => t.subject))
// all parents will be named nodes as they refer to this child node
return Promise.all(triples.map(node => this.hr.node(
node.subject,
{ child: this, rel: node.predicate }
)))
}
function filterChildNodes (triple) {
if (isRdfLiteral(triple.object)) return false
return triple.predicate && triple.predicate !== 'rdf:type'
}
StandardBehavior.properties = async function () {
const triples = await this.hr._get(spo(this.name))
return triples.reduce((p, triple) => {
const value = isRdfLiteral(triple.object) ? fromRdfValue(triple.object) : { name: triple.object }
if (p[triple.predicate] === undefined) p[triple.predicate] = value
else if (Array.isArray(p[triple.predicate])) p[triple.predicate].push(value)
else p[triple.predicate] = [p[triple.predicate], value]
return p
}, {})
}
/** return array of child elements */
StandardBehavior.children = async function (includeLiterals) {
let triples = await this.hr._get(spo(this.name))
// also filter literals out - as they will note be nodes;
if (!includeLiterals) {
const promises = triples
.filter(filterChildNodes)
.map(node => this.hr.node(
node.object,
{ parent: this, rel: node.predicate }
))
return Promise.all(promises)
}
return Promise.all(triples.map(async (node) => {
// console.log('obj', node.object)
if (isRdfLiteral(node.object)) {
return {
attr: node.predicate,
value: node.object
}
}
return this.hr.node(node.object, { parent: this, rel: node.predicate })
}))
}
StandardBehavior.disconnect = async function () {
const parents = await this.parents()
// parents.map(parent => console.log('diconnect', parent.name, parent.rel, this.name))
for (var i = 0; i < parents.length; i++) {
const parent = parents[i]
// console.log('diconnect', parent.name, parent.rel, this.name)
await parent.remove(parent.rel, this)
}
// await Promise.all(parents.map(parent => parent.remove(parent.rel, this)))
return this
}
StandardBehavior.destroy = async function (ignore) {
if (!ignore) ignore = [this.name]
else ignore.push(this.name)
// 1. node.disconnect()
await this.disconnect()
// 2. find all `< node ?a ?b >`
const children = await this.children(true)
for (var i = 0; i < children.length; i++) {
const child = children[i]
if (isNode(child)) {
// 3. for each check if it has other nodes connecting to it eg `< ? ? ?b >`
let itsParents = await child.parents()
itsParents = itsParents.filter(cp => !ignore.includes(cp.name))
if (!itsParents.length) {
// console.log('DESTROY child ->', child.name, child.rel)
await child.destroy(ignore)
} else {
// console.log('DELETE relation to child ->', this.name, child.rel, child.name)
await this.hr._del(spo(this.name, child.rel, child.name))
}
}
await this.hr._del(spo(this.name, child.attr, child.value))
}
}
export default StandardBehavior