UNPKG

tripledoc

Version:

Library to read, create and update documents on a Solid Pod

502 lines (487 loc) 22.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var LinkHeader = _interopDefault(require('http-link-header')); var rdfNamespaces = require('rdf-namespaces'); var rdflib = require('rdflib'); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } var store = rdflib.graph(); var fetcher = new rdflib.Fetcher(store, undefined); var updater = new rdflib.UpdateManager(store); /** * Single instance of an rdflib store, caches all fetched data * * @ignore Can be used as an escape hatch for people who want to use rdflib directly, but if that * is necessary, please consider submitting a feature request describing your use case * on Tripledoc first. */ function getStore() { return store; } /** * Single instance of an rdflib fetcher * * @ignore Can be used as an escape hatch for people who want to use rdflib directly, but if that * is necessary, please consider submitting a feature request describing your use case * on Tripledoc first. */ function getFetcher() { return fetcher; } /** * Single instance of an rdflib updater * * @ignore Can be used as an escape hatch for people who want to use rdflib directly, but if that * is necessary, please consider submitting a feature request describing your use case * on Tripledoc first. */ function getUpdater() { return updater; } /** * Utility function that properly promisifies the RDFLib UpdateManager's update function * * @param statementsToDelete Statements currently present on the Pod that should be deleted. * @param statementsToAdd Statements not currently present on the Pod that should be added. * @returns Promise that resolves when the update was executed successfully, and rejects if not. * @ignore Should not be used by library consumers directly. */ /* istanbul ignore next Just a thin wrapper around rdflib, yet cumbersome to test due to side effects */ function update(statementsToDelete, statementsToAdd) { var promise = new Promise(function (resolve, reject) { var updater = getUpdater(); updater.update(statementsToDelete, statementsToAdd, function (_uri, success, errorBody) { if (success) { return resolve(); } return reject(new Error(errorBody)); }); }); return promise; } /** * @ignore This is a utility method for other parts of the code, and not part of the public API. */ var findSubjectInStore = function (store, predicateRef, objectRef, documentNode) { return findEntityInStore(store, 'subject', null, predicateRef, objectRef, documentNode); }; /** * @ignore This is a utility method for other parts of the code, and not part of the public API. */ var findSubjectsInStore = function (store, predicateRef, objectRef, documentNode) { return findEntitiesInStore(store, 'subject', null, predicateRef, objectRef, documentNode); }; /** * @ignore This is a utility method for other parts of the code, and not part of the public API. */ var findObjectsInStore = function (store, subjectRef, predicateRef, documentNode) { return findEntitiesInStore(store, 'object', subjectRef, predicateRef, null, documentNode); }; /** * @ignore This is a utility method for other parts of the code, and not part of the public API. */ function findEntityInStore(store, type, subjectRef, predicateRef, objectRef, documentNode) { var targetSubject = subjectRef ? rdflib.sym(subjectRef) : null; var targetPredicate = predicateRef ? rdflib.sym(predicateRef) : null; var targetObject = objectRef ? rdflib.sym(objectRef) : null; var targetDocument = rdflib.sym(documentNode); var statement = store.statementsMatching(targetSubject, targetPredicate, targetObject, targetDocument, true)[0]; if (!statement || !statement[type]) { return null; } return normaliseEntity(statement[type]); } /** * @ignore This is a utility method for other parts of the code, and not part of the public API. */ function findEntitiesInStore(store, type, subjectRef, predicateRef, objectRef, documentNode) { var targetSubject = subjectRef ? rdflib.sym(subjectRef) : null; var targetPredicate = predicateRef ? rdflib.sym(predicateRef) : null; var targetObject = objectRef ? rdflib.sym(objectRef) : null; var targetDocument = rdflib.sym(documentNode); var statements = store.statementsMatching(targetSubject, targetPredicate, targetObject, targetDocument, false); return statements.map(function (statement) { return normaliseEntity(statement[type]); }).filter(isEntity); } function normaliseEntity(entity) { if (isNamedNode(entity)) { return entity.uri; } /* istanbul ignore else: All code paths to here result in either a Node or a Literal, so we can't test it */ if (isLiteral(entity)) { return entity; } /* istanbul ignore next: All code paths to here result in either a Node or a Literal, so we can't test it */ return null; } function isEntity(node) { return (node !== null); } /** * @ignore Utility function for working with rdflib, which the library consumer should not need to * be exposed to. */ function isNamedNode(node) { return typeof node.uri === 'string'; } /** * @ignore Only to be called by the Document containing this subject; not a public API. * @param document The Document this Subject is defined in. * @param subjectRef The URL that identifies this subject. */ function initialiseSubject(document, subjectRef) { var store = getStore(); var pendingAdditions = []; var pendingDeletions = []; var get = function (predicateNode) { return findObjectsInStore(store, subjectRef, predicateNode, document.asNodeRef()); }; var getLiteral = function (predicateNode) { var objects = get(predicateNode); var firstLiteral = objects.find(isLiteral); if (typeof firstLiteral === 'undefined') { return null; } return fromLiteral(firstLiteral); }; var getAllLiterals = function (predicateNode) { var objects = get(predicateNode); var literals = objects.filter(isLiteral); return literals.map(fromLiteral); }; var getNodeRef = function (predicateNode) { var objects = get(predicateNode); var firstNodeRef = objects.find(isNodeRef); if (typeof firstNodeRef === 'undefined') { return null; } return firstNodeRef; }; var getAllNodeRefs = function (predicateNode) { var objects = get(predicateNode); var nodeRefs = objects.filter(isNodeRef); return nodeRefs; }; var getType = function () { return getNodeRef(rdfNamespaces.rdf.type); }; var addLiteral = function (predicateRef, literal) { pendingAdditions.push(rdflib.st(rdflib.sym(subjectRef), rdflib.sym(predicateRef), asLiteral(literal), rdflib.sym(document.asNodeRef()))); }; var addNodeRef = function (predicateRef, nodeRef) { pendingAdditions.push(rdflib.st(rdflib.sym(subjectRef), rdflib.sym(predicateRef), rdflib.sym(nodeRef), rdflib.sym(document.asNodeRef()))); }; var removeAll = function (predicateRef) { pendingDeletions.push.apply(pendingDeletions, store.statementsMatching(rdflib.sym(subjectRef), rdflib.sym(predicateRef), null, rdflib.sym(document.asNodeRef()))); }; var subject = { getDocument: function () { return document; }, getStatements: function () { return store.statementsMatching(rdflib.sym(subjectRef), null, null, rdflib.sym(document.asNodeRef())); }, getLiteral: getLiteral, getAllLiterals: getAllLiterals, getNodeRef: getNodeRef, getAllNodeRefs: getAllNodeRefs, getType: getType, addLiteral: addLiteral, addNodeRef: addNodeRef, removeAll: removeAll, removeLiteral: function (predicateRef, literal) { pendingDeletions.push(rdflib.st(rdflib.sym(subjectRef), rdflib.sym(predicateRef), asLiteral(literal), rdflib.sym(document.asNodeRef()))); }, removeNodeRef: function (predicateRef, nodeRef) { pendingDeletions.push(rdflib.st(rdflib.sym(subjectRef), rdflib.sym(predicateRef), rdflib.sym(nodeRef), rdflib.sym(document.asNodeRef()))); }, setLiteral: function (predicateRef, literal) { removeAll(predicateRef); addLiteral(predicateRef, literal); }, setNodeRef: function (predicateRef, nodeRef) { removeAll(predicateRef); addNodeRef(predicateRef, nodeRef); }, getPendingStatements: function () { return [pendingDeletions, pendingAdditions]; }, onSave: function () { pendingDeletions = []; pendingAdditions = []; }, asNodeRef: function () { return subjectRef; }, }; return subject; } function fromLiteral(literal) { if (literal.datatype.uri === 'http://www.w3.org/2001/XMLSchema#dateTime') { // See https://github.com/linkeddata/rdflib.js/blob/d84af88f367b8b5f617c753d8241c5a2035458e8/src/literal.js#L87 var utcFullYear = parseInt(literal.value.substring(0, 4), 10); var utcMonth = parseInt(literal.value.substring(5, 7), 10) - 1; var utcDate = parseInt(literal.value.substring(8, 10), 10); var utcHours = parseInt(literal.value.substring(11, 13), 10); var utcMinutes = parseInt(literal.value.substring(14, 16), 10); var utcSeconds = parseInt(literal.value.substring(17, literal.value.indexOf('Z')), 10); var date = new Date(0); date.setUTCFullYear(utcFullYear); date.setUTCMonth(utcMonth); date.setUTCDate(utcDate); date.setUTCHours(utcHours); date.setUTCMinutes(utcMinutes); date.setUTCSeconds(utcSeconds); return date; } if (literal.datatype.uri === 'http://www.w3.org/2001/XMLSchema#integer') { return parseInt(literal.value, 10); } if (literal.datatype.uri === 'http://www.w3.org/2001/XMLSchema#decimal') { return parseFloat(literal.value); } return literal.value; } function asLiteral(literal) { if (literal instanceof Date) { return rdflib.Literal.fromDate(literal); } if (typeof literal === 'number') { return rdflib.Literal.fromNumber(literal); } return new rdflib.Literal(literal, undefined, undefined); } /** * Initialise a new Turtle document * * Note that this Document will not be created on the Pod until you call [[save]] on it. * * @param ref URL where this document should live * @param statements Initial statements to be included in this document */ function createDocument(ref) { return instantiateDocument(ref, { existsOnPod: false }); } /** * Retrieve a document containing RDF triples * * Note that if you fetch the same document twice, it will be cached; only one * network request will be performed. * * @param documentRef Where the document lives. * @returns Representation of triples in the document at `uri`. */ function fetchDocument(documentRef) { return __awaiter(this, void 0, void 0, function () { var fetcher, response, aclRef; return __generator(this, function (_a) { switch (_a.label) { case 0: fetcher = getFetcher(); return [4 /*yield*/, fetcher.load(documentRef)]; case 1: response = _a.sent(); aclRef = extractAclRef(response, documentRef); return [2 /*return*/, instantiateDocument(documentRef, { aclRef: aclRef, existsOnPod: true })]; } }); }); } function extractAclRef(response, documentRef) { var aclRef; var linkHeader = response.headers.get('Link'); if (linkHeader) { var parsedLinks = LinkHeader.parse(linkHeader); var aclLinks = parsedLinks.get('rel', 'acl'); if (aclLinks.length === 1) { aclRef = new URL(aclLinks[0].uri, documentRef).href; } } return aclRef; } function instantiateDocument(uri, metadata) { var _this = this; var docUrl = new URL(uri); // Remove fragment identifiers (e.g. `#me`) from the URI: var documentRef = docUrl.origin + docUrl.pathname + docUrl.search; var getAclRef = function () { return metadata.aclRef || null; }; var accessedSubjects = {}; var getSubject = function (subjectRef) { if (!accessedSubjects[subjectRef]) { accessedSubjects[subjectRef] = initialiseSubject(tripleDocument, subjectRef); } return accessedSubjects[subjectRef]; }; var findSubject = function (predicateRef, objectRef) { var findSubjectRef = withDocumentSingular(findSubjectInStore, documentRef); var subjectRef = findSubjectRef(predicateRef, objectRef); if (!subjectRef || isLiteral(subjectRef)) { return null; } return getSubject(subjectRef); }; var findSubjects = function (predicateRef, objectRef) { var findSubjectRefs = withDocumentPlural(findSubjectsInStore, documentRef); var subjectRefs = findSubjectRefs(predicateRef, objectRef); return subjectRefs.filter(isNodeRef).map(getSubject); }; var getSubjectsOfType = function (typeRef) { return findSubjects(rdfNamespaces.rdf.type, typeRef); }; var addSubject = function (_a) { var _b = _a === void 0 ? {} : _a, _c = _b.identifier, identifier = _c === void 0 ? generateIdentifier() : _c, _d = _b.identifierPrefix, identifierPrefix = _d === void 0 ? '' : _d; var subjectRef = documentRef + '#' + identifierPrefix + identifier; return getSubject(subjectRef); }; var save = function (subjects) { if (subjects === void 0) { subjects = Object.values(accessedSubjects); } return __awaiter(_this, void 0, void 0, function () { var relevantSubjects, _a, allDeletions, allAdditions, store, updater_1, doc_1, updatePromise, response, aclRef; return __generator(this, function (_b) { switch (_b.label) { case 0: relevantSubjects = subjects.filter(function (subject) { return subject.getDocument().asNodeRef() === documentRef; }); _a = relevantSubjects.reduce(function (_a, subject) { var deletionsSoFar = _a[0], additionsSoFar = _a[1]; var _b = subject.getPendingStatements(), deletions = _b[0], additions = _b[1]; return [deletionsSoFar.concat(deletions), additionsSoFar.concat(additions)]; }, [[], []]), allDeletions = _a[0], allAdditions = _a[1]; if (!!metadata.existsOnPod) return [3 /*break*/, 2]; store = getStore(); updater_1 = getUpdater(); doc_1 = store.sym(documentRef); updatePromise = new Promise(function (resolve, reject) { // Since the Document does not exist remotely yet, // `allDeletions` should be empty and can be ignored: updater_1.put(doc_1, allAdditions, 'text/turtle', function (_uri, ok, errorMessage, response) { if (!ok) { return reject(new Error(errorMessage)); } return resolve(response); }); }); return [4 /*yield*/, updatePromise]; case 1: response = _b.sent(); aclRef = extractAclRef(response, documentRef); if (aclRef) { metadata.aclRef = aclRef; } metadata.existsOnPod = true; return [3 /*break*/, 4]; case 2: return [4 /*yield*/, update(allDeletions, allAdditions)]; case 3: _b.sent(); _b.label = 4; case 4: relevantSubjects.forEach(function (subject) { return subject.onSave(); }); return [2 /*return*/, relevantSubjects]; } }); }); }; var tripleDocument = { addSubject: addSubject, getSubject: getSubject, getSubjectsOfType: getSubjectsOfType, findSubject: findSubject, findSubjects: findSubjects, getAcl: getAclRef, getAclRef: getAclRef, asNodeRef: function () { return documentRef; }, save: save, }; return tripleDocument; } var withDocumentSingular = function (getEntityFromStore, document) { var store = getStore(); return function (knownEntity1, knownEntity2) { return getEntityFromStore(store, knownEntity1, knownEntity2, document); }; }; var withDocumentPlural = function (getEntitiesFromStore, document) { var store = getStore(); return function (knownEntity1, knownEntity2) { return getEntitiesFromStore(store, knownEntity1, knownEntity2, document); }; }; /** * Generate a string that can be used as the unique identifier for a Subject * * This function works by starting with a date string (so that Subjects can be * sorted chronologically), followed by a random number generated by taking a * random number between 0 and 1, and cutting off the `0.`. * * @ignore * @returns An string that's likely to be unique */ var generateIdentifier = function () { return Date.now().toString() + Math.random().toString().substring('0.'.length); }; /** * @ignore Tripledoc's methods should be explicit about whether they return or accept a Literal, so * this is merely an internal utility function, rather than a public API. * @param param A value that might or might not be an RDFlib Literal. * @returns Whether `param` is an RDFlib Literal. */ function isLiteral(param) { return (typeof param === 'object') && (param !== null) && (typeof param.termType === 'string') && param.termType === 'Literal'; } /** * @ignore Tripledoc's methods should be explicit about whether they return or accept a [[NodeRef]], * so this is merely an internal utility function, rather than a public API. * @param param A value that might or might not be a reference to a node in the Linked Data graph. * @returns Whether `param` is a reference to a node in the Linked Data graph. */ function isNodeRef(node) { return typeof node === 'string' && !isLiteral(node); } exports.createDocument = createDocument; exports.fetchDocument = fetchDocument; exports.getFetcher = getFetcher; exports.getStore = getStore; exports.getUpdater = getUpdater; exports.initialiseSubject = initialiseSubject; exports.isLiteral = isLiteral; exports.isNodeRef = isNodeRef; exports.update = update;