modelld
Version:
A JavaScript API for selecting and manipulating linked data subgraphs
487 lines (432 loc) • 18.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Model = undefined;
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
exports.modelFactory = modelFactory;
var _immutable = require('immutable');
var _immutable2 = _interopRequireDefault(_immutable);
var _util = require('./util');
var _field = require('./field');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/**
* A Model represents an RDF subgraph. Specifically, it represents a number of
* RDF quads all relating to the same subject. It allows for convenient
* querying and updating of RDF data in a functional and immutable manner.
*
* @typedef {Object} Model
* @property {Object} subject - The subject of the model represented as an RDF
* NamedNode.
* @property {Immutable.Map<String, Field[]>} _fields - The fields of this model
* keyed by the field keys, which are user-specified aliases for RDF predicates.
* @property {Array[Object]} graveyard - An array of RDF quads which have been
* removed from the model.
*/
/**
* Generates a factory for creating models.
*
* @param {Object} rdf - An RDF library, currently assumed to be rdflib.js.
* @param {Object} fieldMap - A mapping of predicate aliases to RDF predicate
* nodes. For example: { 'name': '<http://xmlns.com/foaf/0.1/name>' }
* @param {Object} fieldCreators - A mapping from field keys to field factory
* functions.
* @returns {Function} - A factory function for creating actual models. The
* factory takes three arguments - an RDF graph object as the data source, the
* URI of the named graph which new fields are added to, and the subject of the
* model as a string.
*/
function modelFactory(rdf, fieldMap) {
return function (graph, defaultNamedGraph, subjectStr) {
var fieldCreators = {};
var subject = rdf.NamedNode.fromValue(subjectStr);
var fields = _immutable2.default.Map(Object.keys(fieldMap).reduce(function (prevFields, fieldName) {
var fieldPredicate = fieldMap[fieldName];
var matchingQuads = graph.statementsMatching(subject, fieldPredicate);
var fieldCreator = (0, _field.fieldFactory)(fieldPredicate);
fieldCreators[fieldName] = fieldCreator;
var matchingFields = matchingQuads.map(function (quad) {
return fieldCreator.fromQuad(quad);
});
return _extends({}, prevFields, _defineProperty({}, fieldName, matchingFields));
}, {}));
// By definition, all the predicates in `fieldMap` must be unique, hence
// inverting the map to have a (predicate -> fieldKey) mapping is safe.
var reverseFieldMap = Object.keys(fieldMap).reduce(function (rdxn, fieldKey) {
return _extends({}, rdxn, _defineProperty({}, fieldMap[fieldKey], fieldKey));
}, {});
return new Model(subject, fields, defaultNamedGraph, [], fieldCreators, reverseFieldMap);
};
}
var Model = exports.Model = function () {
/**
* Creates a model. Requires subject, fields, and optional graveyard.
*
* @constructor
* @param {Object} subject - The subject of this model as an RDF subject
* object.
* @param {Immutable.Map.<String, Field>} fields - a map of field keys to
* field objects. Field keys are aliases for a particular RDF predicate.
* @param {Field[]=} graveyard - An optional array of fields which have been
* removed from the model.
* @returns {Model} the newly constructed model.
*/
function Model(subject, fields, defaultNamedGraph) {
var graveyard = arguments.length <= 3 || arguments[3] === undefined ? [] : arguments[3];
var fieldCreators = arguments.length <= 4 || arguments[4] === undefined ? {} : arguments[4];
var reverseFieldMap = arguments.length <= 5 || arguments[5] === undefined ? {} : arguments[5];
_classCallCheck(this, Model);
this.subject = subject;
this._fields = fields;
this.defaultNamedGraph = defaultNamedGraph;
this.fieldCreators = fieldCreators;
this.reverseFieldMap = reverseFieldMap;
this.graveyard = graveyard;
Object.freeze(this);
}
_createClass(Model, [{
key: 'fromCurrentState',
value: function fromCurrentState(_ref) {
var _ref$fields = _ref.fields;
var fields = _ref$fields === undefined ? this._fields : _ref$fields;
var _ref$defaultNamedGrap = _ref.defaultNamedGraph;
var defaultNamedGraph = _ref$defaultNamedGrap === undefined ? this.defaultNamedGraph : _ref$defaultNamedGrap;
var _ref$graveyard = _ref.graveyard;
var graveyard = _ref$graveyard === undefined ? this.graveyard : _ref$graveyard;
var _ref$fieldCreators = _ref.fieldCreators;
var fieldCreators = _ref$fieldCreators === undefined ? this.fieldCreators : _ref$fieldCreators;
var _ref$reverseFieldMap = _ref.reverseFieldMap;
var reverseFieldMap = _ref$reverseFieldMap === undefined ? this.reverseFieldMap : _ref$reverseFieldMap;
return new Model(this.subject, fields, defaultNamedGraph, graveyard, fieldCreators, reverseFieldMap);
}
/**
* Get all the fields for a given key.
*
* @param {String} key - the key of the fields to look up.
* @returns {Field[]} An array of fields for the given key.
*/
}, {
key: 'fields',
value: function fields(key) {
return this._fields.get(key) || [];
}
/**
* Get all the field values for a given key.
*
* @param {String} key - the key of the fields to look up.
* @returns {String[]} An array of field values for the given key.
*/
}, {
key: 'get',
value: function get(key) {
return this.fields(key).map(function (field) {
return field.value;
});
}
/**
* Get one of the field values for a given key. This just looks for the first
* field value, but order isn't guaranteed.
*
* @param {String} key - the key of the field to look up.
* @returns {String|undefined} The field value for the given key, or undefined
* if none was found.
*/
}, {
key: 'any',
value: function any(key) {
return this.fields(key).map(function (field) {
return field.value;
})[0];
}
/**
* Creates a model with an extra field.
*
* @param {String} key - the key of the fields to add to.
* @param fieldValue - the value of the field to add.
* @returns {Model} - the updated model.
*/
}, {
key: 'add',
value: function add(key, fieldValue) {
var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
var namedGraph = options.namedGraph || this.defaultNamedGraph;
return this.fromCurrentState({
fields: this._fields.set(key, [].concat(_toConsumableArray(this._fields.get(key)), [this.fieldCreators[key](fieldValue, namedGraph, options)]))
});
}
/**
* Adds a field from an RDF quad.
*
* @param {Object} quad - the RDF quad to be converted to a field and added to
* this model.
* @returns {Model} - the updated model.
*/
}, {
key: 'addQuad',
value: function addQuad(quad) {
var key = this.reverseFieldMap[quad.predicate];
return this.fromCurrentState({
fields: this._fields.set(key, [].concat(_toConsumableArray(this._fields.get(key)), [this.fieldCreators[key].fromQuad(quad)]))
});
}
/**
* Creates a model with a removed field.
*
* @param {Field} field - the field to remove.
* @returns {Model} - the updated model.
*/
}, {
key: 'remove',
value: function remove(field) {
if (!this.find(function (f) {
return f.id === field.id;
})) {
return this;
}
return this.filterToGraveyard(function (f) {
return f.id !== field.id;
});
}
/**
* Creates a model with a modified field.
*
* @param {Field} oldField - the field which should be removed.
* @param newFieldValue - the new field's value.
* @param {Object} newFieldOptions - arguments to create the new field.
* @param {String|NamedNode} newFieldOptions.namedGraph - the namedgraph in
* which to store this field, if different from the model's default named
* graph.
* @param {Boolean} newFieldOptions.namedNode - whether the new field is a
* named node or not.
* @returns {Model} - the updated model.
*/
}, {
key: 'set',
value: function set(oldField, newFieldValue, newFieldOptions) {
return this.map(function (field) {
return field.id === oldField.id ? field.set(_extends({}, newFieldOptions, { value: newFieldValue })) : field;
});
}
/**
* Creates a model with a modified field chosen by key. This method should
* only be called with keys for which only one field exists, as there are no
* guarantees for how it picks a field. A new field for the specified key if
* no existing field is found.
*
* @param {String} key - the key of a field to replace.
* @param fieldValue - the new field value.
* @param {Object} fieldOptions - arguments to create the new field.
* @param {String|NamedNode} fieldOptions.namedGraph - the namedgraph in which
* to store this field, if different from the model's default named graph.
* @param {Boolean} fieldOptions.namedNode - whether the new field is a named
* node or not.
* @returns {Model} - the updated model.
*/
}, {
key: 'setAny',
value: function setAny(key, fieldValue, fieldOptions) {
var firstField = this.fields(key)[0];
return firstField ? this.set(firstField, fieldValue, fieldOptions) : this.add(key, fieldValue, fieldOptions);
}
/**
* Compare the current state of the model with its original state and
* determine, for each RDF named graph in the model, which fields should be
* removed and which should be inserted.
*
* @param {Object} rdf - An RDF library, currently assumed to be rdflib.js.
* @param {Model} model - the model.
* @returns {Object} A mapping from graph URIs to the RDF quads (as strings)
* which should be inserted and deleted within those URIs. For example:
* {
* 'http://example.com/one-resource': {
* toIns: [ Field1 ],
* toDel: [ ],
* },
* 'http://example.com/another-resource': {
* toIns: [ ],
* toDel: [ Field2 ],
* },
* }
*/
}, {
key: 'diff',
value: function diff(rdf) {
var _this = this;
var diffMap = this._fields.toArray().reduce(function (reduction, cur) {
return [].concat(_toConsumableArray(reduction), _toConsumableArray(cur));
}).reduce(function (previousMap, field) {
var map = _extends({}, previousMap);
var newQuad = field.toQuad(rdf, _this.subject);
var newSourceURI = newQuad.graph.value;
var originalQuad = field.originalQuad(rdf, _this.subject);
var originalSourceURI = originalQuad ? originalQuad.graph.value : null;
var fieldHasChanged = !originalQuad || !newQuad.equals(originalQuad);
if (fieldHasChanged) {
if (originalQuad) {
if (!(0, _util.isDefined)(map[originalSourceURI])) {
map[originalSourceURI] = { toDel: [], toIns: [] };
}
map[originalSourceURI].toDel.push(originalQuad.toString());
}
if (!(0, _util.isDefined)(map[newSourceURI])) {
map[newSourceURI] = { toDel: [], toIns: [] };
}
map[newSourceURI].toIns.push(newQuad.toString());
}
return map;
}, {});
this.graveyard.forEach(function (field) {
var quad = field.originalQuad(rdf, _this.subject);
if (quad) {
var uri = quad.graph.uri;
if (!(0, _util.isDefined)(diffMap[uri])) {
diffMap[uri] = { toDel: [], toIns: [] };
}
diffMap[uri].toDel.push(quad.toString());
}
}, diffMap);
return diffMap;
}
/**
* Save model updates using an LDP web client.
*
* @param {Object} rdf - An RDF library, currently assumed to be rdflib.js.
* @param {Object} web - A web client library, currently assumed to be
* solid-web-client.
* @param {Model} model - The model to save.
* @returns {Promise<Model>} The updated model.
*/
}, {
key: 'save',
value: function save(rdf, web) {
var _this2 = this;
var diffMap = this.diff(rdf);
var urisToPatch = Object.keys(diffMap);
if (urisToPatch.length === 0) {
return Promise.resolve(this);
}
return patchURIs(rdf, web, diffMap).then(function (patchedURIs) {
var updatedModel = _this2.map(function (field) {
return patchedURIs.has(field.namedGraph.value) ? field.fromCurrentState(rdf, _this2.subject) : field;
}).clearGraveyard();
var allPatchesSucceded = patchedURIs.size === urisToPatch.length;
if (allPatchesSucceded) {
return updatedModel;
} else {
var err = new Error('Not all patches succeeded');
err.model = updatedModel;
err.diffMap = diffMap;
err.failedURIs = new Set(urisToPatch.filter(function (uri) {
return !patchedURIs.has(uri);
}));
throw err;
}
});
}
/**
* Maps a function onto every field, and returns a new model with the
* corresponding fields.
*
* @param {Function(Field)} fn - A function to apply to every field in the
* model. The function receives one argument - the current field.
* @param {Model} model - the model being mapped over.
* @returns {Model} A new model containing the result of the field mapping.
*/
}, {
key: 'map',
value: function map(fn) {
return this.fromCurrentState({
fields: this._fields.map(function (fieldsArray) {
return fieldsArray.map(fn);
})
});
}
/**
* Locate and return the first field on a model that satisfies a predicate
* function.
*
* @param {Function(Field)} - the predicate function which takes each field on
* the model as an argument.
* @returns {Field} - The identified field.
*/
}, {
key: 'find',
value: function find(fn) {
return this._fields.reduce(function (fields, curFieldsArray) {
return [].concat(_toConsumableArray(fields), _toConsumableArray(curFieldsArray));
}).find(function (field) {
return fn(field);
});
}
/**
* Filter fields and mov them to the graveyard if they don't pass a predicate
* function.
*
* @param {Function(Field)} fn - A predicate function returning a Boolean to
* apply to every field in the model. Fields which pass the predicate stay in
* the model, and those which fail the test are removed.
* @returns {Model} A new model with some fields filtered into the graveyard.
*/
}, {
key: 'filterToGraveyard',
value: function filterToGraveyard(fn) {
var removedFields = [];
var newFields = this._fields.map(function (fieldsArray) {
return fieldsArray.filter(function (field) {
var testPassed = fn(field);
if (!testPassed) {
removedFields.push(field);
}
return testPassed;
});
});
return this.fromCurrentState({
fields: newFields,
graveyard: [].concat(_toConsumableArray(this.graveyard), removedFields)
});
}
/**
* Returns a new model with the same fields but an empty graveyard.
*
* @returns {Model} - The new graveyard-less model
*/
}, {
key: 'clearGraveyard',
value: function clearGraveyard() {
return this.fromCurrentState({ graveyard: [] });
}
}]);
return Model;
}();
/**
* Given a diff map (from Model.diff), patch each resource in the diff map using
* the web client's patch method. Return a Promise which resolves to the set of
* URIs which were successfully patched. Note that the Promise does not reject;
* if some resources failed to be patched, they're not included in the URI set.
*
* @param {Object} rdf - An RDF library, currently assumed to be rdflib.js.
* @param {Object} web - A web client library, currently assumed to be
* @param {Object} diffMap - The result of running Model.diff() on a model.
* @returns {Promise<Set<String>>} A Promise which always resolves to the set of
* URIs for which the patches succeeded.
*/
function patchURIs(rdf, web, diffMap) {
var urisToPatch = Object.keys(diffMap);
return Promise.all(urisToPatch.map(function (uri) {
return web.patch(uri, diffMap[uri].toDel, diffMap[uri].toIns).then(function (solidResponse) {
return { URI: solidResponse.url, succeeded: true };
}).catch(function (solidResponse) {
return { URI: solidResponse.url, succeeded: false };
});
})).then(function (sourceResults) {
return new Set(sourceResults.filter(function (result) {
return result.succeeded;
}).map(function (result) {
return result.URI;
}));
});
}