tripledoc
Version:
Library to read, create and update documents on a Solid Pod
502 lines (487 loc) • 22.8 kB
JavaScript
;
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;