UNPKG

@treecg/curation

Version:

This library implements a class (Curator) with methods to curate an announcement LDES in LDP.

853 lines (852 loc) 48.9 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; 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 __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Curator = void 0; var ldes_announcements_1 = require("@treecg/ldes-announcements"); var ldes_orchestrator_1 = require("@treecg/ldes-orchestrator"); var tree_metadata_extraction_1 = require("@treecg/tree-metadata-extraction"); var Logger_Browser_1 = require("@treecg/types/dist/lib/utils/Logger-Browser"); var n3_1 = require("n3"); var Conversion_1 = require("./util/Conversion"); var SolidCommunication_1 = require("./util/SolidCommunication"); var Vocabularies_1 = require("./util/Vocabularies"); var namedNode = n3_1.DataFactory.namedNode; var literal = n3_1.DataFactory.literal; var Curator = /** @class */ (function () { // this field allows waiting for the synchronization process. function Curator(config, session) { this.logger = new Logger_Browser_1.LoggerBrowser(this); this.curatedIRI = config.curatedIRI; this.ldesIRI = config.ldesIRI; this.synchronizedIRI = config.synchronizedIRI; this.session = session; } /** * Checks whether the user is logged in * Load the curatedLDES in Solid * Creates the curatedLDES in Solid if it does not exist (requires certain access control permissions on the curated IRI) * Furthermore verifies that the LDES exists as well. * @param privateCuration When true, the curated LDES in LDP is only visible to the webId from the session. (Default: True) * @return {Promise<void>} */ Curator.prototype.init = function (privateCuration) { return __awaiter(this, void 0, void 0, function () { var config, e_1, ldesStore, e_2, treeShape, relations, bn, relationType, treePath; return __generator(this, function (_a) { switch (_a.label) { case 0: privateCuration = privateCuration !== undefined ? privateCuration : true; if (!this.session.info.isLoggedIn) { this.logger.error("Contents of the session: ".concat(JSON.stringify(this.session.info))); throw Error("Session is not logged in"); } config = undefined; _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, ldes_orchestrator_1.LDESinSolid.getConfig(this.curatedIRI, this.session)]; case 2: config = _a.sent(); return [3 /*break*/, 4]; case 3: e_1 = _a.sent(); this.logger.info("No curated LDES in LDP exist yet at ".concat(this.curatedIRI)); return [3 /*break*/, 4]; case 4: _a.trys.push([4, 6, , 7]); return [4 /*yield*/, (0, SolidCommunication_1.fetchResourceAsStore)("".concat(this.ldesIRI, "root.ttl"), this.session)]; case 5: ldesStore = _a.sent(); return [3 /*break*/, 7]; case 6: e_2 = _a.sent(); this.logger.error("LDES in LDP does not exist at ".concat(this.ldesIRI)); throw Error("LDES in LDP does not exist"); case 7: if (!config) return [3 /*break*/, 8]; this.curatedLDESinSolid = new ldes_orchestrator_1.LDESinSolid(config.ldesConfig, config.aclConfig, this.session); return [3 /*break*/, 13]; case 8: treeShape = ldesStore.getObjects(null, Vocabularies_1.TREE.shape, null); relations = ldesStore.getObjects(null, Vocabularies_1.TREE.relation, null); if (relations.length === 0) { throw Error('Original LDES root node currently no relations.'); } if (treeShape.length === 0) { throw Error('Original LDES has currently no shape.'); } bn = relations[0].id; relationType = ldesStore.getObjects(bn, Vocabularies_1.RDF.type, null); treePath = ldesStore.getObjects(null, Vocabularies_1.TREE.path, null); config = { aclConfig: { agent: this.session.info.webId // I know it exists }, ldesConfig: { base: this.curatedIRI, treePath: treePath[0].id, shape: 'https://tree.linkeddatafragments.org/datasets/shape', relationType: relationType[0].id, } }; this.curatedLDESinSolid = new ldes_orchestrator_1.LDESinSolid(config.ldesConfig, config.aclConfig, this.session); if (!privateCuration) return [3 /*break*/, 10]; return [4 /*yield*/, this.curatedLDESinSolid.createLDESinLDP(ldes_orchestrator_1.AccessSubject.Agent)]; case 9: _a.sent(); return [3 /*break*/, 12]; case 10: return [4 /*yield*/, this.curatedLDESinSolid.createLDESinLDP()]; case 11: _a.sent(); _a.label = 12; case 12: this.logger.info("Created curated LDES in Solid at ".concat(this.curatedIRI)); _a.label = 13; case 13: return [2 /*return*/]; } }); }); }; Curator.prototype.accept = function (memberIRI, member, timestamp) { return __awaiter(this, void 0, void 0, function () { var extracted, containerIRI, text, response, e_3; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.curatedLDESinSolid) { throw Error("First execute function init() as the curated LDES was not initialised yet"); } if (!!timestamp) return [3 /*break*/, 2]; return [4 /*yield*/, this.getTimestamp(memberIRI)]; case 1: timestamp = _a.sent(); _a.label = 2; case 2: if (!!member) return [3 /*break*/, 4]; return [4 /*yield*/, this.extractMember(memberIRI)]; case 3: extracted = _a.sent(); member = extracted.value; _a.label = 4; case 4: return [4 /*yield*/, this.curatedLDESinSolid.getCurrentContainer()]; case 5: containerIRI = _a.sent(); this.logger.debug("Posting contents of ".concat(member["@id"], " to ").concat(containerIRI, ".")); return [4 /*yield*/, (0, Conversion_1.memberToString)(member, memberIRI)]; case 6: text = _a.sent(); _a.label = 7; case 7: _a.trys.push([7, 10, , 11]); this.logger.info("Accepted ".concat(memberIRI, " to the curated ldes located at ").concat(this.curatedIRI)); return [4 /*yield*/, (0, SolidCommunication_1.postResource)(containerIRI, this.session, text, 'text/turtle')]; case 8: response = _a.sent(); return [4 /*yield*/, this.removeFromSyncedCollection(memberIRI, timestamp)]; case 9: _a.sent(); return [2 /*return*/, response]; case 10: e_3 = _a.sent(); this.logger.error('Something failed'); console.log(e_3); throw Error("Could not add the member to ".concat(this.curatedIRI)); case 11: return [2 /*return*/]; } }); }); }; /** * Curation action reject: Which means that a member is removed from the synced collection * @param memberIRI * @param timestamp * @returns {Promise<Response>} */ Curator.prototype.reject = function (memberIRI, timestamp) { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!!timestamp) return [3 /*break*/, 2]; return [4 /*yield*/, this.getTimestamp(memberIRI)]; case 1: timestamp = _a.sent(); _a.label = 2; case 2: return [4 /*yield*/, this.removeFromSyncedCollection(memberIRI, timestamp)]; case 3: response = _a.sent(); return [2 /*return*/, response]; } }); }); }; /** * Fetch the timestamp of an IRI by reading the Last-modified header. * For a member in an LDES, this will be the same as the creation time (as member are immutable * , indicating that they MUST NOT be changed after creation) * @param iri IRI of the LDP Resource * @returns {Promise<number>} */ Curator.prototype.getTimestamp = function (iri) { return __awaiter(this, void 0, void 0, function () { var response, lastModifiedHeader, dateLastModified; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.session.fetch(iri, { method: "HEAD" })]; case 1: response = _a.sent(); lastModifiedHeader = response.headers.get('Last-modified'); if (!lastModifiedHeader) throw Error("Resource ".concat(iri, " has no Last-modified header.")); dateLastModified = new Date(lastModifiedHeader); return [2 /*return*/, dateLastModified.getTime()]; } }); }); }; /** * Removes a member from the synced collection stored at the syncedURI * @param memberIRI IRI of the member * @param timestamp timestamp of creation of the member in the LDES in LDP * @returns {Promise<Response>} */ Curator.prototype.removeFromSyncedCollection = function (memberIRI, timestamp) { return __awaiter(this, void 0, void 0, function () { var syncedLocation, memberQuad, timeQuad, response; return __generator(this, function (_a) { switch (_a.label) { case 0: syncedLocation = memberIRI.replace(this.ldesIRI, this.synchronizedIRI).split('/').slice(0, -1).join('/'); memberQuad = new n3_1.Quad(namedNode("".concat(this.ldesIRI, "root.ttl#Collection")), namedNode(Vocabularies_1.TREE.member), namedNode(memberIRI)); timeQuad = new n3_1.Quad(namedNode(memberIRI), namedNode(Vocabularies_1.DCT.modified), this.timestampToLiteral(timestamp)); return [4 /*yield*/, (0, SolidCommunication_1.patchQuads)(syncedLocation, this.session, [memberQuad, timeQuad], SolidCommunication_1.SPARQL.DELETE)]; case 1: response = _a.sent(); this.logger.info("Removed ".concat(memberIRI, " from the synced collection at ").concat(this.synchronizedIRI)); return [2 /*return*/, response]; } }); }); }; /** * Synchronize the Synced collection with the LDES in LDP * * The synced collection is a TREE collection with as members the URIs of the LDES in LDP. Each member also has a timestamp. * A synchronize operations looks at the last synced time and adds all members which were added to the LDES to the synced collection. * * @returns {Promise<void>} */ Curator.prototype.synchronize = function () { return __awaiter(this, void 0, void 0, function () { var LDESRootNode, LDESRootCollectionIRI, syncedRootNode, response, contentType, body, store; return __generator(this, function (_a) { switch (_a.label) { case 0: LDESRootNode = "".concat(this.ldesIRI, "root.ttl"); LDESRootCollectionIRI = "".concat(LDESRootNode, "#Collection"); syncedRootNode = "".concat(this.synchronizedIRI, "root.ttl"); return [4 /*yield*/, this.session.fetch(syncedRootNode)]; case 1: response = _a.sent(); contentType = response.headers.get('content-type'); if (!contentType) { throw Error("No content-type known of ".concat(syncedRootNode)); } if (!(response.status === 200)) return [3 /*break*/, 5]; return [4 /*yield*/, response.text()]; case 2: body = _a.sent(); return [4 /*yield*/, (0, Conversion_1.stringToStore)(body, { contentType: contentType, baseIRI: syncedRootNode })]; case 3: store = _a.sent(); return [4 /*yield*/, this.otherTimesSync(LDESRootNode, syncedRootNode, LDESRootCollectionIRI, store)]; case 4: _a.sent(); return [3 /*break*/, 8]; case 5: // create container return [4 /*yield*/, (0, SolidCommunication_1.putContainer)(this.synchronizedIRI, this.session)]; case 6: // create container _a.sent(); return [4 /*yield*/, this.firstTimeSync(LDESRootNode, syncedRootNode, LDESRootCollectionIRI)]; case 7: _a.sent(); _a.label = 8; case 8: return [2 /*return*/]; } }); }); }; /** * Extract the member and its metadata from an LDES in LDP. Also extract the announcement itself * Currently, only View, DataSet and DataService can be parsed (interface can be found in LDES-Announcements) * @param announcementIRI * @returns {Promise<{iri: string, type: string, value: View, announcement: Announce} | {iri: string, type: string, value: DataSet, announcement: Announce} | {iri: string, type: string, value: DataService, announcement: Announce}>} */ Curator.prototype.extractMember = function (announcementIRI) { return __awaiter(this, void 0, void 0, function () { var memberStore, metadata, announcementIRIs, iri, announcement, valueIRI, type, content, content, content; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, (0, SolidCommunication_1.fetchResourceAsStore)(announcementIRI, this.session)]; case 1: memberStore = _a.sent(); return [4 /*yield*/, (0, ldes_announcements_1.extractAnnouncementsMetadata)(memberStore)]; case 2: metadata = _a.sent(); announcementIRIs = []; metadata.announcements.forEach(function (announcement) { announcementIRIs.push(announcement["@id"]); }); if (announcementIRIs.length !== 1) { throw Error("There is more than one announcement in ".concat(announcementIRI)); } iri = announcementIRIs[0]; announcement = metadata.announcements.get(iri); if (!announcement) throw Error("Announcement was not correct ".concat(iri)); valueIRI = announcement.object["@id"]; type = memberStore.getObjects(valueIRI, Vocabularies_1.RDF.type, null).map(function (object) { return object.id; }); if (type.includes(Vocabularies_1.TREE.Node)) { this.logger.debug("View from ".concat(announcementIRI, " extracted.")); content = metadata.views.get(valueIRI); return [2 /*return*/, { type: Vocabularies_1.TREE.Node, value: content, iri: announcementIRI, announcement: announcement }]; } if (type.includes(Vocabularies_1.DCAT.Dataset)) { this.logger.debug("DCAT dataset from ".concat(announcementIRI, " extracted.")); content = metadata.datasets.get(valueIRI); return [2 /*return*/, { type: Vocabularies_1.DCAT.Dataset, value: content, iri: announcementIRI, announcement: announcement }]; } if (type.includes(Vocabularies_1.DCAT.DataService)) { this.logger.debug("DCAT Dataservice ".concat(announcementIRI, " extracted.")); content = metadata.dataServices.get(valueIRI); return [2 /*return*/, { type: Vocabularies_1.DCAT.DataService, value: content, iri: announcementIRI, announcement: announcement }]; } throw Error("Could not extract member from ".concat(announcementIRI)); } }); }); }; /** * Flow for the first time an LDES in LDP has to be synchronized * * @param LDESRootNode IRI of the LDES in LDP root node * @param syncedRootNode IRI of the root node of the synced collection * @param LDESRootCollectionIRI IRI of the LDES in LDP collection */ Curator.prototype.firstTimeSync = function (LDESRootNode, syncedRootNode, LDESRootCollectionIRI) { return __awaiter(this, void 0, void 0, function () { var LDESRootStore, metadata, metadataRelations, collection, view, now, relations, body, bodyStore, turtleBody, error_1, relationNodeIds, _i, relationNodeIds_1, relationNodeId; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, (0, SolidCommunication_1.fetchResourceAsStore)(LDESRootNode, this.session)]; case 1: LDESRootStore = _a.sent(); return [4 /*yield*/, (0, tree_metadata_extraction_1.extractMetadata)(LDESRootStore.getQuads(null, null, null, null))]; case 2: metadata = _a.sent(); metadataRelations = metadata.nodes.get(LDESRootNode).relation; collection = { "@context": { "@vocab": Vocabularies_1.TREE.namespace }, "@id": "".concat(syncedRootNode, "#Collection"), "@type": [Vocabularies_1.TREE.Collection], view: [{ "@id": syncedRootNode }] }; view = { "@context": { "@vocab": Vocabularies_1.TREE.namespace }, "@id": syncedRootNode, "@type": [Vocabularies_1.TREE.Node], relation: [] }; now = new Date(); view[Vocabularies_1.DCT.issued] = { '@value': now.toISOString(), '@type': Vocabularies_1.XSD.dateTime }; relations = []; metadataRelations.forEach(function (relationIRI) { var _a; // clone the relation -> I don't want to update it var relation = __assign({}, metadata.relations.get(relationIRI['@id'])); // In practice this if holds always if (relation.node && relation.node[0]) { // change relation of node -> again clone it otherwise the original node is changed relation.node = __spreadArray([], relation.node, true); relation.node[0] = { "@id": _this.ldesRelationToSyncedRelationIRI(relation.node[0]['@id']) }; } (_a = view.relation) === null || _a === void 0 ? void 0 : _a.push(relationIRI); // I just defined it, believe me it's there relations.push(relation); }); body = __spreadArray([collection, view], relations, true); return [4 /*yield*/, (0, Conversion_1.ldjsonToStore)(JSON.stringify(body))]; case 3: bodyStore = _a.sent(); turtleBody = (0, Conversion_1.storeToString)(bodyStore); _a.label = 4; case 4: _a.trys.push([4, 6, , 7]); return [4 /*yield*/, (0, SolidCommunication_1.putTurtle)(syncedRootNode, this.session, turtleBody)]; case 5: _a.sent(); return [3 /*break*/, 7]; case 6: error_1 = _a.sent(); console.log(error_1); this.logger.error("Could not create root at ".concat(syncedRootNode)); return [3 /*break*/, 7]; case 7: relationNodeIds = []; LDESRootStore.getObjects(LDESRootNode, Vocabularies_1.TREE.relation, null).forEach(function (object) { var relationNodeId = LDESRootStore.getObjects(object, Vocabularies_1.TREE.node, null)[0].id; relationNodeIds.push(relationNodeId); }); _i = 0, relationNodeIds_1 = relationNodeIds; _a.label = 8; case 8: if (!(_i < relationNodeIds_1.length)) return [3 /*break*/, 11]; relationNodeId = relationNodeIds_1[_i]; return [4 /*yield*/, this.synchronizeMembersFromRelation(relationNodeId, LDESRootCollectionIRI)]; case 9: _a.sent(); _a.label = 10; case 10: _i++; return [3 /*break*/, 8]; case 11: return [2 /*return*/]; } }); }); }; /** * Flow for the subsequent times an LDES in LDP has to be synchronized * * @param LDESRootNode IRI of the LDES in LDP root node * @param syncedRootNode IRI of the root node of the synced collection * @param LDESRootCollectionIRI IRI of the LDES in LDP collection * @param syncedStore The store of the synced root node */ Curator.prototype.otherTimesSync = function (LDESRootNode, syncedRootNode, LDESRootCollectionIRI, syncedStore) { return __awaiter(this, void 0, void 0, function () { var syncQuads, lastSynced, nowQuad, newRelations, _i, newRelations_1, relation, iri, error_2, error_3; return __generator(this, function (_a) { switch (_a.label) { case 0: syncQuads = syncedStore.getQuads(syncedRootNode, Vocabularies_1.DCT.issued, null, null); if (syncQuads.length !== 1) { throw Error("Can't find last time synced at ".concat(syncedRootNode, ".")); } lastSynced = this.extractTimeFromLiteral(syncQuads[0].object); nowQuad = new n3_1.Quad(namedNode(syncedRootNode), namedNode(Vocabularies_1.DCT.issued), this.timestampToLiteral(Date.now())); return [4 /*yield*/, this.updateRootNode(LDESRootNode, syncedStore, syncedRootNode)]; case 1: newRelations = _a.sent(); _i = 0, newRelations_1 = newRelations; _a.label = 2; case 2: if (!(_i < newRelations_1.length)) return [3 /*break*/, 5]; relation = newRelations_1[_i]; iri = this.syncedRelationtoLDESRelationIRI(relation); return [4 /*yield*/, this.synchronizeMembersFromRelation(iri, LDESRootCollectionIRI)]; case 3: _a.sent(); _a.label = 4; case 4: _i++; return [3 /*break*/, 2]; case 5: return [4 /*yield*/, this.updateCurrentMostRecentRelation(syncedStore, lastSynced, LDESRootCollectionIRI)]; case 6: _a.sent(); _a.label = 7; case 7: _a.trys.push([7, 9, , 10]); return [4 /*yield*/, (0, SolidCommunication_1.patchQuads)(syncedRootNode, this.session, [nowQuad], SolidCommunication_1.SPARQL.INSERT)]; case 8: _a.sent(); return [3 /*break*/, 10]; case 9: error_2 = _a.sent(); console.log(error_2); this.logger.error("Could not add new DCTERMS issued at ".concat(syncedRootNode)); return [3 /*break*/, 10]; case 10: _a.trys.push([10, 12, , 13]); return [4 /*yield*/, (0, SolidCommunication_1.patchQuads)(syncedRootNode, this.session, [syncQuads[0]], SolidCommunication_1.SPARQL.DELETE)]; case 11: _a.sent(); return [3 /*break*/, 13]; case 12: error_3 = _a.sent(); console.log(error_3); this.logger.error("Could not delete old DCTERMS issued at ".concat(syncedRootNode)); return [3 /*break*/, 13]; case 13: this.logger.info("Sync time updated in Synced root at ".concat(syncedRootNode)); return [2 /*return*/]; } }); }); }; Curator.prototype.updateCurrentMostRecentRelation = function (syncedStore, lastSynced, LDESRootCollectionIRI) { return __awaiter(this, void 0, void 0, function () { var syncedMostRecentRelation, mostRecentRelation, store, potentialMembers, members, collection, collectionIRI, error_4; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: syncedMostRecentRelation = this.calculateMostRecentRelation(syncedStore); mostRecentRelation = this.syncedRelationtoLDESRelationIRI(syncedMostRecentRelation); return [4 /*yield*/, (0, SolidCommunication_1.fetchResourceAsStore)(mostRecentRelation, this.session)]; case 1: store = _a.sent(); potentialMembers = store.getObjects(null, Vocabularies_1.LDP.contains, null).map(function (object) { return object.id; }); members = []; potentialMembers.forEach(function (memberID) { var memberTimeLiteral = store.getQuads(memberID, Vocabularies_1.DCT.modified, null, null)[0].object; var memberDateTime = _this.extractTimeFromLiteral(memberTimeLiteral); if (memberDateTime > lastSynced) { members.push(memberID); } }); collection = new n3_1.Store(); collection.addQuad(namedNode(LDESRootCollectionIRI), namedNode(Vocabularies_1.RDF.type), namedNode(Vocabularies_1.TREE.Collection)); members.forEach(function (member) { var dateTimeLiteral = store.getObjects(member, Vocabularies_1.DCT.modified, null)[0]; if (!dateTimeLiteral) throw Error("Announcement has no dc:modified ".concat(member)); collection.addQuad(namedNode(LDESRootCollectionIRI), namedNode(Vocabularies_1.TREE.member), namedNode(member)); collection.addQuad(namedNode(member), namedNode(Vocabularies_1.DCT.modified), dateTimeLiteral); // Also add time to curated IRI }); collectionIRI = this.ldesRelationToSyncedRelationIRI(mostRecentRelation); _a.label = 2; case 2: _a.trys.push([2, 4, , 5]); return [4 /*yield*/, (0, SolidCommunication_1.patchQuads)(collectionIRI, this.session, collection.getQuads(null, null, null, null), SolidCommunication_1.SPARQL.INSERT)]; case 3: _a.sent(); return [3 /*break*/, 5]; case 4: error_4 = _a.sent(); console.log(error_4); this.logger.error("Could not update part of collection at ".concat(collectionIRI)); return [3 /*break*/, 5]; case 5: this.logger.info("".concat(collectionIRI, " was updated with ").concat(collection.getQuads(null, Vocabularies_1.TREE.member, null, null).length, " members.")); return [2 /*return*/]; } }); }); }; /** * Updates the synced root node with the new relations since last sync time. * Also returns the new relation nodes which members should be added to the synced collection as well. * * @param LDESRootNode * @param syncedStore * @param syncedRootNode * @returns {Promise<string[]>} */ Curator.prototype.updateRootNode = function (LDESRootNode, syncedStore, syncedRootNode) { return __awaiter(this, void 0, void 0, function () { var LDESRootStore, metadata, newRelationsStore, newRelations, metadataRelations, error_5; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, (0, SolidCommunication_1.fetchResourceAsStore)(LDESRootNode, this.session)]; case 1: LDESRootStore = _a.sent(); return [4 /*yield*/, (0, tree_metadata_extraction_1.extractMetadata)(LDESRootStore.getQuads(null, null, null, null))]; case 2: metadata = _a.sent(); newRelationsStore = new n3_1.Store(); newRelations = []; metadataRelations = metadata.nodes.get(LDESRootNode).relation; metadataRelations.forEach(function (relationIRI) { var relation = metadata.relations.get(relationIRI["@id"]); // They should exist, but still a check? if (!(relation.node && relation.node[0] && relation.path && relation.path[0] && relation.value && relation.value[0] && relation["@type"] && relation["@type"][0])) { throw Error('relation parts are not in this relation'); } var node = __assign({}, relation.node[0]); node = { "@id": _this.ldesRelationToSyncedRelationIRI(node['@id']) }; var exists = syncedStore.getQuads(null, Vocabularies_1.TREE.node, node["@id"], null).length === 1; // only if it doesn't exist yet, should the root be patched if (!exists) { var relationNode = newRelationsStore.createBlankNode(); newRelationsStore.addQuad(namedNode(syncedRootNode), namedNode(Vocabularies_1.TREE.relation), relationNode); newRelationsStore.addQuad(relationNode, namedNode(Vocabularies_1.RDF.type), namedNode(relation["@type"][0])); newRelationsStore.addQuad(relationNode, namedNode(Vocabularies_1.TREE.node), namedNode(node["@id"])); newRelationsStore.addQuad(relationNode, namedNode(Vocabularies_1.TREE.path), namedNode(relation.path[0]["@id"])); var dateTime = new Date(relation.value[0]['@value']); newRelationsStore.addQuad(relationNode, namedNode(Vocabularies_1.TREE.value), _this.timestampToLiteral(dateTime.getTime())); newRelations.push(node["@id"]); } }); _a.label = 3; case 3: _a.trys.push([3, 5, , 6]); return [4 /*yield*/, (0, SolidCommunication_1.patchQuads)(syncedRootNode, this.session, newRelationsStore.getQuads(null, null, null, null), SolidCommunication_1.SPARQL.INSERT)]; case 4: _a.sent(); return [3 /*break*/, 6]; case 5: error_5 = _a.sent(); console.log(error_5); this.logger.error("Could not patch root at ".concat(syncedRootNode)); return [3 /*break*/, 6]; case 6: this.logger.info("Root patched with new relations at ".concat(syncedRootNode)); return [2 /*return*/, newRelations]; } }); }); }; /** * Retrieve the URIs of the most recent members of the synced collection. * This method can be further optimised as currently the whole collection is fetched even though a part is wanted * @param amount number of members that are needed * @param startPoint Startpoint in the synced collection * @returns {Promise<{timestamp: number, memberIRI: string}[]>} sorted list (newest members first) */ Curator.prototype.getRecentMembers = function (amount, startPoint) { return __awaiter(this, void 0, void 0, function () { var syncedRootIRI, memberStore, e_4, syncedMetadata, relationIRIs, promiseStores, _i, relationIRIs_1, iri, timeSortedMembers; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: syncedRootIRI = "".concat(this.synchronizedIRI, "root.ttl"); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, (0, SolidCommunication_1.fetchResourceAsStore)(syncedRootIRI, this.session)]; case 2: memberStore = _a.sent(); return [3 /*break*/, 4]; case 3: e_4 = _a.sent(); this.logger.error('Synchronized root does not exist yet. Call the synchronize() method first before members of the LDES can be retrieved'); return [2 /*return*/, []]; case 4: return [4 /*yield*/, (0, tree_metadata_extraction_1.extractMetadata)(memberStore.getQuads(null, null, null, null))]; case 5: syncedMetadata = _a.sent(); relationIRIs = []; syncedMetadata.relations.forEach(function (relation) { return relationIRIs.push(relation.node[0]["@id"]); }); promiseStores = []; for (_i = 0, relationIRIs_1 = relationIRIs; _i < relationIRIs_1.length; _i++) { iri = relationIRIs_1[_i]; promiseStores.push((0, SolidCommunication_1.fetchResourceAsStore)(iri, this.session)); } return [4 /*yield*/, Promise.all(promiseStores).then(function (stores) { stores.forEach(function (store) { memberStore.addQuads(store.getQuads(null, null, null, null)); }); })]; case 6: _a.sent(); timeSortedMembers = []; memberStore.getQuads("".concat(this.ldesIRI, "root.ttl#Collection"), Vocabularies_1.TREE.member, null, null).forEach(function (member) { var memberIRI = member.object.id; var timeliteral = memberStore.getObjects(memberIRI, Vocabularies_1.DCT.modified, null)[0]; timeSortedMembers.push({ timestamp: _this.extractTimeFromLiteral(timeliteral), memberIRI: memberIRI }); }); this.logger.info("Members extracted from ".concat(syncedRootIRI)); // sort them timeSortedMembers.sort(function (first, second) { return second.timestamp - first.timestamp; }); // extract the ones asked in the function and return them return [2 /*return*/, timeSortedMembers.slice(startPoint, amount)]; } }); }); }; /** * Convert resources in a container of the LDES in LDP to members of the collection and return as a store * @param store store of the LDP Container * @param iri iri of the LDP Container * @param LDESRootCollectionIRI iri of the LDES in LDP EventStream * @returns {Store} */ Curator.prototype.extractMembers = function (store, iri, LDESRootCollectionIRI) { var members = store.getObjects(null, Vocabularies_1.LDP.contains, null) .map(function (object) { return object.id; }); var collection = new n3_1.Store(); collection.addQuad(namedNode(LDESRootCollectionIRI), namedNode(Vocabularies_1.RDF.type), namedNode(Vocabularies_1.TREE.Collection)); members.forEach(function (member) { var dateTimeLiteral = store.getObjects(member, Vocabularies_1.DCT.modified, null)[0]; if (!dateTimeLiteral) throw Error("Announcement has no dc:modified ".concat(iri).concat(member)); collection.addQuad(namedNode(LDESRootCollectionIRI), namedNode(Vocabularies_1.TREE.member), namedNode(member)); collection.addQuad(namedNode(member), namedNode(Vocabularies_1.DCT.modified), dateTimeLiteral); // Also add time to curated IRI }); return collection; }; /** * Extract the members of a relation node in the LDES in LDP (where the relation node is an LDP Container) * And Put all those members together with their created time in the synchronized collection * @param iri IRI of an LDP container (which is the relation node) * @param LDESCollectionIRI The IRI of the Event Stream (=Collection) of the LDES in LDP * @returns {Promise<void>} */ Curator.prototype.synchronizeMembersFromRelation = function (iri, LDESCollectionIRI) { return __awaiter(this, void 0, void 0, function () { var store, collection, body, collectionIRI, error_6; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, (0, SolidCommunication_1.fetchResourceAsStore)(iri, this.session)]; case 1: store = _a.sent(); collection = this.extractMembers(store, iri, LDESCollectionIRI); body = (0, Conversion_1.storeToString)(collection); collectionIRI = this.ldesRelationToSyncedRelationIRI(iri); _a.label = 2; case 2: _a.trys.push([2, 4, , 5]); return [4 /*yield*/, (0, SolidCommunication_1.putTurtle)(collectionIRI, this.session, body)]; case 3: _a.sent(); return [3 /*break*/, 5]; case 4: error_6 = _a.sent(); console.log(error_6); this.logger.error("Could not update part of collection at ".concat(collectionIRI)); return [3 /*break*/, 5]; case 5: return [2 /*return*/]; } }); }); }; /** * Using a store containing several relations, calculate the most recent relation * @param syncedStore * @returns {any} */ Curator.prototype.calculateMostRecentRelation = function (syncedStore) { var _this = this; var relationValues = syncedStore.getQuads(null, Vocabularies_1.TREE.value, null, null); var maxValue = 0; var relationMap = new Map(); relationValues.forEach(function (quad) { var time = _this.extractTimeFromLiteral(quad.object); if (time >= maxValue) { maxValue = time; } var nodeId = syncedStore.getQuads(quad.subject, Vocabularies_1.TREE.node, null, null)[0].object.id; relationMap.set(time, nodeId); }); var mostRecentRelation = relationMap.get(maxValue); if (!mostRecentRelation) { throw Error('not possible'); } return mostRecentRelation; }; /** * Extract a timestamp (ms) from an RDF Literal * @param dateTimeLiteral * @returns {number} */ Curator.prototype.extractTimeFromLiteral = function (dateTimeLiteral) { var value = dateTimeLiteral.value; if (!(dateTimeLiteral.datatype && dateTimeLiteral.datatype.id === Vocabularies_1.XSD.dateTime)) { throw Error("Could not interpret ".concat(dateTimeLiteral, " as it was not ").concat(Vocabularies_1.XSD.dateTime)); } var dateTime = new Date(value); return dateTime.getTime(); }; /** * Convert a timestamp (ms) to an RDF Literal * @param timestamp * @returns {Literal} */ Curator.prototype.timestampToLiteral = function (timestamp) { var dateTime = new Date(timestamp); return literal(dateTime.toISOString(), namedNode(Vocabularies_1.XSD.dateTime)); }; /** * Transforms an ldes relation iri to a synchronized relation iri * E.g. ldesIRI is "https://tree.linkeddatafragments.org/announcements/" * and synchronizedIRI is "https://tree.linkeddatafragments.org/datasets/synced/" * Then "https://tree.linkeddatafragments.org/announcements/1636985640000/" becomes * "https://tree.linkeddatafragments.org/datasets/synced/1636985640000" * @param iri * @returns {string} */ Curator.prototype.ldesRelationToSyncedRelationIRI = function (iri) { return iri.replace(this.ldesIRI, this.synchronizedIRI).slice(0, -1); }; /** * Transforms a synchronized relation iri to an ldes relation iri * @param iri * @returns {string} */ Curator.prototype.syncedRelationtoLDESRelationIRI = function (iri) { return "".concat(iri.replace(this.synchronizedIRI, this.ldesIRI), "/"); }; return Curator; }()); exports.Curator = Curator;