@ldhop/core
Version:
Follow your nose through linked data resources - core
382 lines • 17 kB
JavaScript
import { NamedNode, Quad, Store } from 'n3';
import { removeHashFromURI } from './utils/helpers.js';
const stringifyQuad = (quad) => JSON.stringify(quad.toJSON());
const quadElements = ['subject', 'predicate', 'object', 'graph'];
const metaUris = {
meta: 'https://ldhop.example/meta',
status: 'https://ldhop.example/status',
missing: 'https://ldhop.example/status/missing',
added: 'https://ldhop.example/status/added',
failed: 'https://ldhop.example/status/failed',
resource: 'https://ldhop.example/resource',
variable: 'https://ldhop.example/variable',
};
class Variable extends NamedNode {
constructor(variable) {
super(metaUris.variable + '/' + variable);
this.variable = variable;
}
static getVar(term) {
return term.value.split('/').pop();
}
}
const meta = (Object.fromEntries(Object.entries(metaUris).map(([key, uri]) => [key, new NamedNode(uri)])));
class Moves {
constructor() {
this.list = new Set();
this.provides = {};
this.providersOf = {};
this.byQuad = {};
/* this is a debugging feature, it will return a list of current moves as a string */
this.print = () => {
let output = '';
this.list.forEach(move => {
const from = Object.values(move.from)
.flatMap(f => Array.from(f))
.map(f => f.value);
const to = Object.values(move.to)
.flatMap(f => Array.from(f))
.map(f => f.value);
output += from.concat(' ') + ' ==> ' + to.concat(' ');
});
return output;
};
}
add(move) {
var _a;
var _b, _c;
// add step to list
this.list.add(move);
// add step to "provides" index
Object.values(move.from).forEach(terms => {
terms.forEach(term => {
var _a;
var _b, _c;
(_a = (_b = this.provides)[_c = term.value]) !== null && _a !== void 0 ? _a : (_b[_c] = new Set());
this.provides[term.value].add(move);
});
});
// add step to "providersOf" index
Object.values(move.to).forEach(terms => {
terms.forEach(term => {
var _a;
var _b, _c;
(_a = (_b = this.providersOf)[_c = term.value]) !== null && _a !== void 0 ? _a : (_b[_c] = new Set());
this.providersOf[term.value].add(move);
});
});
// add step to byQuad index
if (move.quad) {
(_a = (_b = this.byQuad)[_c = stringifyQuad(move.quad)]) !== null && _a !== void 0 ? _a : (_b[_c] = new Set());
this.byQuad[stringifyQuad(move.quad)].add(move);
}
}
remove(move) {
this.list.delete(move);
// remove step from "provides" index
Object.values(move.from).forEach(terms => {
terms.forEach(term => {
this.provides[term.value].delete(move);
});
});
// remove step from "providersOf" index
Object.values(move.to).forEach(terms => {
terms.forEach(term => {
this.providersOf[term.value].delete(move);
});
});
// remove step from byQuad index
if (move.quad) {
this.byQuad[stringifyQuad(move.quad)].delete(move);
}
}
}
/**
* @deprecated use LdhopEngine instead
*/
export class QueryAndStore {
constructor(query, startingPoints, store = new Store()) {
this.moves = new Moves();
this.store = store;
this.query = query;
Object.entries(startingPoints).forEach(([variable, uris]) => {
variable = variable.startsWith('?') ? variable.slice(1) : variable;
uris.forEach(uri => {
// we add a move for each variable that is provided at the beginning
// sometimes circular reference would try to remove them
// we prevent that by making sure the initial variables don't get orphaned, with this move
const term = new NamedNode(uri);
this.moves.add({
from: {},
to: { [variable]: new Set([term]) },
step: -1,
});
this.addVariable(variable, term);
});
});
}
getMissingResources() {
return this.getResources('missing');
}
removeVariable(variable, node) {
var _a;
this.store.removeQuad(new Quad(node, meta.variable, new Variable(variable), meta.meta));
if (node.termType === 'NamedNode') {
const resourceNode = new NamedNode(removeHashFromURI(node.value));
this.store.removeQuad(new Quad(node, meta.resource, resourceNode, meta.meta));
this.store.removeQuads(this.store.getQuads(resourceNode, meta.status, null, meta.meta));
this.removeResource(resourceNode.value);
}
// if the removed variable leads through some step to other variable, & nothing else leads to that variable, remove that variable
const uri = node.value;
const movesFromVariable = Array.from((_a = this.moves.provides[uri]) !== null && _a !== void 0 ? _a : []).filter(move => { var _a; return Array.from((_a = move.from[variable]) !== null && _a !== void 0 ? _a : []).some(term => term.equals(node)); });
for (const move of movesFromVariable) {
const nextVariables = move.to;
this.moves.remove(move);
for (const variable in nextVariables) {
nextVariables[variable].forEach(nextTerm => {
var _a;
// see if there's any provision of this variable left
const a = Array.from((_a = this.moves.providersOf[nextTerm.value]) !== null && _a !== void 0 ? _a : new Set()).filter(a => { var _a; return Array.from((_a = a.to[variable]) !== null && _a !== void 0 ? _a : []).some(t => t.equals(nextTerm)); });
if (a.length === 0)
this.removeVariable(variable, nextTerm);
});
}
}
}
addResource(resource, quads, status = 'success') {
const resourceNode = new NamedNode(resource);
const oldResource = this.store.getQuads(null, null, null, resourceNode);
const [additions, deletions] = quadDiff(quads, oldResource);
additions.forEach(quad => this.addQuad(quad));
deletions.forEach(quad => this.removeQuad(quad));
const missing = new Quad(resourceNode, meta.status, meta.missing, meta.meta);
const added = new Quad(resourceNode, meta.status, meta.added, meta.meta);
const failed = new Quad(resourceNode, meta.status, meta.failed, meta.meta);
// mark the resource as added or failed depending on status
this.store.removeQuads([missing, added, failed]);
if (status === 'success')
this.store.addQuad(added);
if (status === 'error')
this.store.addQuad(failed);
}
addQuad(quad) {
// find relevant matches in steps
this.store.addQuad(quad);
const variables = Object.fromEntries(quadElements.map(el => [
el,
this.store.getObjects(quad[el], meta.variable, meta.meta),
]));
const matchQuadElement = (step, element) => {
const el = step[element];
const node = quad[element];
if (!el)
return true;
if (el.startsWith('?')) {
if (variables[element].some(v => v.equals(new Variable(el.slice(1)))))
return true;
}
if (el === node.value)
return true;
return false;
};
// find relevant steps
const relevantSteps = Object.entries(this.query)
.filter((entry) => typeof entry[1] !== 'function' && entry[1].type === 'match')
.filter(([, step]) =>
// keep only steps that match given quad.
quadElements.every(element => matchQuadElement(step, element)))
.map(([i, s]) => [+i, s]);
// hop the steps and assign new variables
for (const [i, step] of relevantSteps) {
// save the move
const from = {};
for (const element of quadElements) {
const el = step[element];
if (el === null || el === void 0 ? void 0 : el.startsWith('?'))
from[el.slice(1)] = new Set([quad[element]]);
}
const to = { [step.target.slice(1)]: new Set([quad[step.pick]]) };
this.moves.add({ step: i, from, to, quad });
this.addVariable(step.target.slice(1), quad[step.pick]);
}
// when assigning new variables, make hops from the new variables, too
}
removeResource(uri) {
const quads = this.store.getQuads(null, null, null, new NamedNode(uri));
quads.forEach(q => this.removeQuad(q));
}
removeQuad(quad) {
var _a;
this.store.removeQuad(quad);
// is there a move that was made thanks to this quad?
const moves = (_a = this.moves.byQuad[stringifyQuad(quad)]) !== null && _a !== void 0 ? _a : new Set();
// now, for each move, let's go through the provided variables and remove those that have nothing; then remove the move
moves.forEach(move => {
const providedVariables = move.to;
this.moves.remove(move);
// now, remove variables that are no longer provided by any move.
for (const variable in providedVariables) {
providedVariables[variable].forEach(term => {
// which moves provide this variable
const providingMoves = Array.from(this.moves.providersOf[term.value]).filter(a => { var _a; return Array.from((_a = a.to[variable]) !== null && _a !== void 0 ? _a : []).some(t => t.equals(term)); });
if (providingMoves.length === 0)
this.removeVariable(variable, term);
});
}
});
}
hopFromVariable(variable, node) {
// find steps relevant for this variable
const qVariable = `?${variable}`;
const relevantSteps = this.query
.map((s, i) => [s, i])
.filter(([step]) => {
if (typeof step === 'function')
return true;
if (step.type === 'match' &&
quadElements.some(el => step[el] === qVariable))
return true;
if (step.type === 'transform variable' && step.source === qVariable)
return true;
return false;
});
relevantSteps.forEach(([step, i]) => {
var _a;
if (typeof step === 'function') {
step(this);
}
else if (step.type === 'transform variable') {
const transformedNode = step.transform(node);
if (transformedNode) {
this.moves.add({
from: { [step.source.slice(1)]: new Set([node]) },
to: {
[step.target.slice(1)]: new Set([transformedNode]),
},
step: i,
});
this.addVariable(step.target.slice(1), transformedNode);
}
}
else if (step.type === 'match') {
// try to match quad(s) relevant for this step
const generateRules = (step, element) => {
let outputs = new Set();
const s = step[element];
if (!s)
outputs.add(null);
else if (!s.startsWith('?'))
outputs.add(new NamedNode(s));
else if (s.slice(1) === variable)
outputs.add(node);
else {
const variables = this.store.getSubjects(meta.variable, new Variable(s.slice(1)), meta.meta);
outputs = new Set(variables);
}
return outputs;
};
const constraints = Object.fromEntries(quadElements.map(el => [el, generateRules(step, el)]));
for (const s of constraints.subject) {
for (const p of constraints.predicate) {
for (const o of constraints.object) {
for (const g of constraints.graph) {
const quads = this.store.getQuads(s, p, o, g);
for (const quad of quads) {
const targetVar = step.target.slice(1);
const target = quad[step.pick];
const from = {};
for (const el of quadElements)
if ((_a = step[el]) === null || _a === void 0 ? void 0 : _a.startsWith('?'))
from[step[el].slice(1)] = new Set([quad[el]]);
const to = { [targetVar]: new Set([target]) };
this.moves.add({ from, to, step: i, quad });
this.addVariable(targetVar, target);
}
}
}
}
}
}
});
}
addVariable(variable, node) {
// if the variable is already added, there's nothing to do
if (this.store.has(new Quad(node, meta.variable, new Variable(variable), meta.meta)))
return;
this.store.addQuads([
// add the new variable
new Quad(node, meta.variable, new Variable(variable), meta.meta),
]);
let resourceNode = undefined;
if (node.termType === 'NamedNode') {
resourceNode = new NamedNode(removeHashFromURI(node.value));
this.store.addQuads([
// make a resource
new Quad(node, meta.resource, resourceNode, meta.meta),
]);
}
// if the variable is used in other queries and hasn't been given status, mark it as missing
const qVariable = `?${variable}`;
const isInAddResources = this.query.some(a => typeof a !== 'function' &&
a.type === 'add resources' &&
a.variable === qVariable);
const isInMatch = this.query.some(a => typeof a !== 'function' &&
a.type === 'match' &&
quadElements.some(el => a[el] === qVariable));
const isNeeded = isInAddResources || isInMatch;
if (resourceNode &&
isNeeded &&
this.store.match(resourceNode, meta.status, null, meta.meta).size === 0) {
this.store.addQuad(resourceNode, meta.status, meta.missing, meta.meta);
}
this.hopFromVariable(variable, node);
}
/**
* If you provide variable name, this method will return URIs that belong to that variable
*/
getVariable(variableName) {
if (variableName.startsWith('?'))
variableName = variableName.slice(1);
return this.store
.getSubjects(meta.variable, new Variable(variableName), meta.meta)
.map(s => s.value);
}
getAllVariables() {
return this.store
.getQuads(null, meta.variable, null, meta.meta)
.reduce((dict, quad) => {
var _a;
var _b;
(_a = dict[_b = Variable.getVar(quad.object)]) !== null && _a !== void 0 ? _a : (dict[_b] = new Set());
dict[Variable.getVar(quad.object)].add(quad.subject.value);
return dict;
}, {});
}
getResources(status) {
if (!status)
return this.store
.getObjects(null, meta.resource, meta.meta)
.map(s => s.value);
const statusNode = status === 'missing'
? meta.missing
: status === 'failed'
? meta.failed
: meta.added;
return this.store
.getSubjects(meta.status, statusNode, meta.meta)
.map(m => m.value);
}
}
const quadDiff = (newQuads, oldQuads) => {
// quickly bail out in simple cases
if (oldQuads.length === 0)
return [newQuads, []];
if (newQuads.length === 0)
return [[], oldQuads];
// additions are new quads minus old quads
const additions = newQuads.filter(nq => !oldQuads.some(oq => nq.equals(oq)));
const deletions = oldQuads.filter(oq => !newQuads.some(nq => oq.equals(nq)));
return [additions, deletions];
};
//# sourceMappingURL=QueryAndStore.js.map