UNPKG

@treecg/ldes-orchestrator

Version:

Fills the gaps that a Linked Data Platform (LDP) cannot do by itself for creating a Linked Data Event Stream (LDES) in LDP.

503 lines (502 loc) 26.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 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) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LDESinSolid = void 0; var Logger_Browser_1 = require("@treecg/types/dist/lib/utils/Logger-Browser"); var n3_1 = require("n3"); var rdf_parse_1 = __importDefault(require("rdf-parse")); var Acl_1 = require("./util/Acl"); var EventStream_1 = require("./util/EventStream"); var Vocabularies_1 = require("./util/Vocabularies"); var parse = require('parse-link-header'); var storeStream = require("rdf-store-stream").storeStream; var streamify = require('streamify-string'); var LDESinSolid = /** @class */ (function () { function LDESinSolid(ldesConfig, aclConfig, session, amount) { this.logger = LDESinSolid.staticLogger; this._ldesConfig = ldesConfig; this._aclConfig = aclConfig; this._session = session; if (amount) { this._amount = amount; } else { this._amount = 100; } } Object.defineProperty(LDESinSolid.prototype, "ldesConfig", { get: function () { return this._ldesConfig; }, enumerable: false, configurable: true }); Object.defineProperty(LDESinSolid.prototype, "aclConfig", { get: function () { return this._aclConfig; }, enumerable: false, configurable: true }); Object.defineProperty(LDESinSolid.prototype, "session", { get: function () { return this._session; }, enumerable: false, configurable: true }); Object.defineProperty(LDESinSolid.prototype, "amount", { get: function () { return this._amount; }, enumerable: false, configurable: true }); LDESinSolid.getConfig = function (base, session) { return __awaiter(this, void 0, void 0, function () { var rootIRI, rootStore, aclStore, shapeIRI, relation, relationType, treePath, ldesConfig, aclConfig; return __generator(this, function (_a) { switch (_a.label) { case 0: rootIRI = "".concat(base, "root.ttl"); return [4 /*yield*/, LDESinSolid.fetchStore(rootIRI, session)]; case 1: rootStore = _a.sent(); return [4 /*yield*/, LDESinSolid.fetchStore("".concat(base, ".acl"), session)]; case 2: aclStore = _a.sent(); shapeIRI = rootStore.getQuads("".concat(rootIRI, "#Collection"), Vocabularies_1.TREE.shape, null, null)[0].object.id; relation = rootStore.getQuads(rootIRI, Vocabularies_1.TREE.relation, null, null)[0].object.id; relationType = rootStore.getQuads(relation, Vocabularies_1.RDF.type, null, null)[0].object.id; treePath = rootStore.getQuads(relation, Vocabularies_1.TREE.path, null, null)[0].object.id; ldesConfig = { base: base, relationType: relationType, shape: shapeIRI, treePath: treePath }; aclConfig = { agent: aclStore.getQuads(null, Vocabularies_1.ACL.agent, null, null)[0].object.id }; return [2 /*return*/, { ldesConfig: ldesConfig, aclConfig: aclConfig }]; } }); }); }; LDESinSolid.prototype.getAmountResources = function () { return __awaiter(this, void 0, void 0, function () { var currentContainerLocation, store, resources; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.getCurrentContainer()]; case 1: currentContainerLocation = _a.sent(); return [4 /*yield*/, LDESinSolid.fetchStore(currentContainerLocation, this.session)]; case 2: store = _a.sent(); resources = store.getQuads(currentContainerLocation, Vocabularies_1.LDP.contains, null, null); return [2 /*return*/, resources.length]; } }); }); }; LDESinSolid.prototype.getCurrentContainer = function () { return __awaiter(this, void 0, void 0, function () { var headResponse, linkHeaders, inboxLink; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.session.fetch(this.ldesConfig.base, { method: 'HEAD' })]; case 1: headResponse = _a.sent(); linkHeaders = parse(headResponse.headers.get('link')); if (!linkHeaders) { throw new Error('No Link Header present.'); } inboxLink = linkHeaders[Vocabularies_1.LDP.inbox]; if (!inboxLink) { throw new Error('No http://www.w3.org/ns/ldp#inbox Link Header present.'); } return [2 /*return*/, "".concat(inboxLink.url)]; } }); }); }; /** * Fetches the iri and transforms the contents to a N3 Store * Note: currently only works for text/turle * @param iri * @param session * @returns {Promise<Store>} */ LDESinSolid.fetchStore = function (iri, session) { return __awaiter(this, void 0, void 0, function () { var response, _a, _b, currentContainerText, textStream, quadStream, store; return __generator(this, function (_c) { switch (_c.label) { case 0: return [4 /*yield*/, session.fetch(iri, { method: "GET", headers: { Accept: "text/turtle" } })]; case 1: response = _c.sent(); if (!(response.status !== 200)) return [3 /*break*/, 3]; _b = (_a = this.staticLogger).info; return [4 /*yield*/, response.text()]; case 2: _b.apply(_a, [_c.sent()]); throw Error("Fetching ".concat(iri, " to parse it into an N3 Store has failed.")); case 3: return [4 /*yield*/, response.text()]; case 4: currentContainerText = _c.sent(); textStream = streamify(currentContainerText); quadStream = rdf_parse_1.default.parse(textStream, { contentType: 'text/turtle', baseIRI: iri }); return [4 /*yield*/, storeStream(quadStream)]; case 5: store = _c.sent(); return [2 /*return*/, store]; } }); }); }; /** * Creates a container. Only succeeds when a new container was created * @param iri * @param session * @returns {Promise<void>} */ LDESinSolid.createContainer = function (iri, session) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, session.fetch(iri, { method: "PUT", headers: { Link: '<http://www.w3.org/ns/ldp#Container>; rel="type"', "Content-Type": 'text/turtle' } })]; case 1: response = _a.sent(); if (response.status !== 201) { if (response.status === 205) { throw Error("Root \"".concat(iri, "\" already exists | status code: ").concat(response.status)); } throw Error("Root \"".concat(iri, "\" was not created | status code: ").concat(response.status)); } this.staticLogger.info("LDP container created: ".concat(response.url)); return [2 /*return*/]; } }); }); }; LDESinSolid.updateAcl = function (aclIRI, aclBody, session) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, session.fetch(aclIRI, { method: "PUT", headers: { 'Content-Type': 'application/ld+json', Link: '<http://www.w3.org/ns/ldp#Resource>; rel="type"' }, body: JSON.stringify(aclBody) })]; case 1: response = _a.sent(); if (!(response.status === 201 || response.status === 205)) { throw Error("Creating/Updating the ACL file (".concat(aclIRI, ") was not successful | Status code: ").concat(response.status)); } return [2 /*return*/, response]; } }); }); }; LDESinSolid.addShape = function (iri, shapeIRI, session) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, session.fetch(iri, { method: "PUT", headers: { Link: "<".concat(shapeIRI, ">; rel=\"").concat(Vocabularies_1.LDP.constrainedBy, "\""), "Content-Type": 'text/turtle' } })]; case 1: response = _a.sent(); if (response.status !== 205) { throw Error("Adding the shape to the container (".concat(iri, ") was not successful | status code: ").concat(response.status)); } this.staticLogger.info("Shape validation added to ".concat(response.url)); return [2 /*return*/]; } }); }); }; LDESinSolid.updateInbox = function (iri, inboxIRI, session) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, session.fetch(iri, { method: "PUT", headers: { Link: "<".concat(inboxIRI, ">; rel=\"").concat(Vocabularies_1.LDP.inbox, "\""), "Content-Type": 'text/turtle' } })]; case 1: response = _a.sent(); if (response.status !== 205) { throw Error("Updating the inbox was not successful | Status code: ".concat(response.status)); } this.staticLogger.info("".concat(iri, " is now the inbox of the LDES.")); return [2 /*return*/]; } }); }); }; LDESinSolid.addRelation = function (iri, ldesConfig, session) { return __awaiter(this, void 0, void 0, function () { var rootIRI, rootStore, regex, newNodeName, writer, rootText, updateRootResponse; return __generator(this, function (_a) { switch (_a.label) { case 0: rootIRI = "".concat(ldesConfig.base, "root.ttl"); return [4 /*yield*/, this.fetchStore(rootIRI, session)]; case 1: rootStore = _a.sent(); regex = /\/([^/]*)\/$/.exec(iri); if (!regex) throw Error("expected \"".concat(iri, "\" to be an IRI.")); newNodeName = regex[1]; (0, EventStream_1.addRelation)(rootStore, ldesConfig.treePath, ldesConfig.relationType, newNodeName, ldesConfig.base); writer = new n3_1.Writer(); rootText = writer.quadsToString(rootStore.getQuads(null, null, null, null)); return [4 /*yield*/, session.fetch(rootIRI, { method: "PUT", headers: { "Content-Type": 'text/turtle', Link: '<http://www.w3.org/ns/ldp#Resource>; rel="type"', }, body: rootText })]; case 2: updateRootResponse = _a.sent(); if (updateRootResponse.status !== 205) { throw Error("Updating the LDES root was not successful | Status code: ".concat(updateRootResponse.status)); } this.staticLogger.info("".concat(updateRootResponse.url, " is updated with a new relation to ").concat(iri, ".")); return [2 /*return*/]; } }); }); }; /** * Creates a new LDES in LDP. * First the ldp:Container is created where everything will reside. * Then a new container is added as defined in the UML sequence diagram for LDES in LDP. * Finally a root is created (instead of updated). * * When the public can append to the new container, @param accessSubject should be AccessSubject.Public or left blank. * When only the owner can append to the new container, it should be AccessSubject.Agent. * * @param accessSubject * @returns {Promise<void>} */ LDESinSolid.prototype.createLDESinLDP = function (accessSubject) { return __awaiter(this, void 0, void 0, function () { var aclRootBody, firstContainerName, firstContainerIRI, aclNewBody, eventStream, writer, rootText, postRootResponse; return __generator(this, function (_a) { switch (_a.label) { case 0: accessSubject = accessSubject !== undefined ? accessSubject : Acl_1.AccessSubject.Public; // create root container return [4 /*yield*/, LDESinSolid.createContainer(this.ldesConfig.base, this.session)]; case 1: // create root container _a.sent(); aclRootBody = this.createACLBody(accessSubject, Acl_1.AccessMode.Read); return [4 /*yield*/, LDESinSolid.updateAcl("".concat(this.ldesConfig.base, ".acl"), aclRootBody, this.session)]; case 2: _a.sent(); firstContainerName = new Date().getTime().toString(); firstContainerIRI = "".concat(this.ldesConfig.base + firstContainerName, "/"); // create first container return [4 /*yield*/, LDESinSolid.createContainer(firstContainerIRI, this.session)]; case 3: // create first container _a.sent(); // add shape triple to container .meta return [4 /*yield*/, LDESinSolid.addShape(firstContainerIRI, this.ldesConfig.shape, this.session)]; case 4: // add shape triple to container .meta _a.sent(); // change inbox header in root container .meta return [4 /*yield*/, LDESinSolid.updateInbox(this.ldesConfig.base, firstContainerIRI, this.session)]; case 5: // change inbox header in root container .meta _a.sent(); aclNewBody = this.createACLBody(accessSubject, Acl_1.AccessMode.ReadAppend); return [4 /*yield*/, LDESinSolid.updateAcl("".concat(firstContainerIRI, ".acl"), aclNewBody, this.session)]; case 6: _a.sent(); return [4 /*yield*/, (0, EventStream_1.createEventStream)(this.ldesConfig.shape, this.ldesConfig.treePath, firstContainerName, this.ldesConfig.base)]; case 7: eventStream = _a.sent(); writer = new n3_1.Writer(); rootText = writer.quadsToString(eventStream.getQuads(null, null, null, null)); return [4 /*yield*/, this.session.fetch(this.ldesConfig.base, { method: "POST", headers: { "Content-Type": 'text/turtle', Link: '<http://www.w3.org/ns/ldp#Resource>; rel="type"', slug: 'root.ttl' }, body: rootText })]; case 8: postRootResponse = _a.sent(); if (postRootResponse.status !== 201) { throw Error("Creating root.ttl was not successful | Status code: ".concat(postRootResponse.status)); } this.logger.info("".concat(postRootResponse.url, " is the EventStream and view of the LDES in LDP.")); return [2 /*return*/]; } }); }); }; /** * Creates a new container when the old container is deemed full. * It follows the sequence described in the UML sequence diagram for LDES in LDP. * * When the public can append to the new container, @param accessSubject should be AccessSubject.Public or left blank. * When only the owner can append to the new container, it should be AccessSubject.Agent. * * @param accessSubject * @returns {Promise<void>} */ LDESinSolid.prototype.createNewContainer = function (accessSubject) { return __awaiter(this, void 0, void 0, function () { var currentContainerAmountResources, oldContainer, newContainerName, newContainerIRI, aclNewBody, aclCurrentBody; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.getAmountResources()]; case 1: currentContainerAmountResources = _a.sent(); return [4 /*yield*/, this.getCurrentContainer()]; case 2: oldContainer = _a.sent(); accessSubject = accessSubject !== undefined ? accessSubject : Acl_1.AccessSubject.Public; if (currentContainerAmountResources < this.amount) { this.logger.info("No need for orchestrating as current amount of resources (".concat(currentContainerAmountResources, ") is less than the maximum allowed amount of resources per container (").concat(this.amount, ")")); return [2 /*return*/]; } this.logger.info("Current amount of resources (".concat(currentContainerAmountResources, ") is greater or equal than the maximum allowed amount of resources per container (").concat(this.amount, ").")); this.logger.info("Creating new container as inbox has started:"); newContainerName = new Date().getTime().toString(); newContainerIRI = "".concat(this.ldesConfig.base + newContainerName, "/"); // create new container return [4 /*yield*/, LDESinSolid.createContainer(newContainerIRI, this.session)]; case 3: // create new container _a.sent(); // add shape triple to container .meta return [4 /*yield*/, LDESinSolid.addShape(newContainerIRI, this.ldesConfig.shape, this.session)]; case 4: // add shape triple to container .meta _a.sent(); aclNewBody = this.createACLBody(accessSubject, Acl_1.AccessMode.ReadAppend); return [4 /*yield*/, LDESinSolid.updateAcl("".concat(newContainerIRI, ".acl"), aclNewBody, this.session)]; case 5: _a.sent(); // change inbox header in root container .meta return [4 /*yield*/, LDESinSolid.updateInbox(this.ldesConfig.base, newContainerIRI, this.session)]; case 6: // change inbox header in root container .meta _a.sent(); aclCurrentBody = this.createACLBody(accessSubject, Acl_1.AccessMode.Read); return [4 /*yield*/, LDESinSolid.updateAcl("".concat(oldContainer, ".acl"), aclCurrentBody, this.session)]; case 7: _a.sent(); // update relation in root.ttl return [4 /*yield*/, LDESinSolid.addRelation(newContainerIRI, this.ldesConfig, this.session)]; case 8: // update relation in root.ttl _a.sent(); return [2 /*return*/]; } }); }); }; /** * Create the AclBody * When the subject is public, everybody is allowed to interact with the accompanying resources * @param accessSubject * @param accessMode mode for interacting with the accompanying resource * @returns {Acl[]} */ LDESinSolid.prototype.createACLBody = function (accessSubject, accessMode) { var aclBody = []; // always allow that the agent has control over the resources aclBody.push((0, Acl_1.createAclContent)('#orchestrator', [Vocabularies_1.ACL.Read, Vocabularies_1.ACL.Write, Vocabularies_1.ACL.Control], this.aclConfig.agent)); if (accessSubject === Acl_1.AccessSubject.Public) { switch (accessMode) { case Acl_1.AccessMode.ReadAppend: aclBody.push((0, Acl_1.createAclContent)('#authorization', [Vocabularies_1.ACL.Read, Vocabularies_1.ACL.Append])); break; case Acl_1.AccessMode.Read: aclBody.push((0, Acl_1.createAclContent)('#authorization', [Vocabularies_1.ACL.Read])); break; default: } } return aclBody; }; LDESinSolid.staticLogger = new Logger_Browser_1.LoggerBrowser(LDESinSolid.name); return LDESinSolid; }()); exports.LDESinSolid = LDESinSolid;