UNPKG

@ldhop/core

Version:

Follow your nose through linked data resources - core

382 lines 17 kB
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