@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
JavaScript
"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;