semantic-network
Version:
A utility library for manipulating a list of links that form a semantic interface to a network of resources.
470 lines • 25.6 kB
JavaScript
"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 __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
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));
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.pooledSingletonMakeStrategy = exports.pooledCollectionMakeStrategy = exports.defaultMakeStrategy = exports.SparseRepresentationFactory = void 0;
var semantic_link_1 = require("semantic-link");
var types_1 = require("../types/types");
var state_1 = require("./state");
var status_1 = require("./status");
var anylogger_1 = __importDefault(require("anylogger"));
var instanceOfFeed_1 = require("../utils/instanceOf/instanceOfFeed");
var instanceOfCollection_1 = require("../utils/instanceOf/instanceOfCollection");
var linkRelation_1 = require("../linkRelation");
var canonicalOrSelf_1 = require("../utils/comparators/canonicalOrSelf");
var trackedRepresentationUtil_1 = require("../utils/trackedRepresentationUtil");
var instanceOfTrackedRepresentation_1 = require("../utils/instanceOf/instanceOfTrackedRepresentation");
var log = (0, anylogger_1.default)('SparseRepresentationFactory');
/**
* A factory for performing the initial construction of tracked resources.
*
* This factory is responsible for performing a logical 'new' of the in memory object
* used to represent resources. These objects have a 'state' object of type {@link State}
* that is used to track and manage the resources.
*
* If the across the wire representation is available it can be provided via
* the {@link ResourceFactoryOptions.addStateOn} property to provide an initial values
* for the resource. If the values are not provided then the resource is marked
* as being in the {@link Status.locationOnly} state (i.e. sparely populated).
*
* This factory is a pluggable strategy via the {@link ResourceFactoryOptions.makeSparseStrategy}. By
* default, the strategy is to create new instances for every instance of a resource. Implementations
* are provided to allowed pooled instances.
*/
var SparseRepresentationFactory = exports.SparseRepresentationFactory = /** @class */ (function () {
function SparseRepresentationFactory() {
}
/**
* A simple facade to allow the make() method to be provided by an alternative strategy.
*
* @see {defaultMakeStrategy}
*/
SparseRepresentationFactory.make = function (options) {
var _a;
// Get the optional makeSparse strategy defaulting to the standard default implementation below.
var makeSparseStrategy = (_a = __assign({}, options).makeSparseStrategy, _a === void 0 ? SparseRepresentationFactory.defaultMakeStrategy : _a);
return makeSparseStrategy(options);
};
/**
* Returns a {@link LinkedRepresentation} with {@link State} initialised. An initialised representation will
* be at worst sparse with a state ({@link Status.locationOnly}, {@link Status.virtual}). At best, the representation
* is {@link Status.hydrated} when a resource is presented that has been retrieved across the wire.
*/
SparseRepresentationFactory.defaultMakeStrategy = function (options) {
var _a = __assign({}, options), addStateOn = _a.addStateOn, opts = __rest(_a, ["addStateOn"]);
if (addStateOn) {
return SparseRepresentationFactory.makeHydrated(addStateOn, opts);
}
else {
return SparseRepresentationFactory.makeSparse(opts);
}
};
/**
* Create sparse items from a 'pool'. The pool is a single collection resource, which is used
* as both a source of items and a location to store new (sparse) items.
*
* This strategy allows the caching of resources in a memory conservative way so that the same
* resource is not loaded twice. More importantly this also means that if the application/user
* has a view of those resources then the view will be the same.
*
* It is assumed that the pooled collection is logically backed/represented by the set of all
* possible items, whereas the specific collection is a subset of those items.
*/
SparseRepresentationFactory.pooledCollectionMakeStrategy = function (pool, options) {
var addStateOn = __assign({}, options).addStateOn;
if (addStateOn) {
return SparseRepresentationFactory.makeHydratedPoolCollection(addStateOn, pool, options);
}
else {
return SparseRepresentationFactory.makeSparse(__assign(__assign({}, options), { addStateOn: undefined }));
}
};
/**
* Find the first matching item in a collection. Match by URI.
*
* At this stage, it is really unlikely that this will ever match on eTag for identity at this point. ETag
* checking is later on in the pipeline (ie versioning is later)
*/
SparseRepresentationFactory.firstMatchingFeedItem = function (collection, id) {
return collection
.items
.find(function (anItem) { return semantic_link_1.LinkUtil.getUri(anItem, canonicalOrSelf_1.CanonicalOrSelf) === id; });
};
/**
* Create sparse item from a 'pool'. The pool is a single collection resource, which is used
* as both a source of items and a location to store new (sparse) items.
*
* This strategy allows the caching of resources in a memory conservative way so that the same
* resource is not loaded twice. More importantly this also means that if the application/user
* has a view of those resources then the view will be the same.
*
* It is assumed that the pooled collection is logically backed/represented by the set of all
* possible items, whereas the specific collection is a subset of those items.
*/
SparseRepresentationFactory.pooledSingletonMakeStrategy = function (pool, options) {
var _a = __assign({}, options), addStateOn = _a.addStateOn, opts = __rest(_a, ["addStateOn"]);
if (addStateOn) {
return SparseRepresentationFactory.makeHydratedPoolSingleton(addStateOn, pool, opts);
}
else {
return SparseRepresentationFactory.makeSparsePooled(pool, opts);
}
};
SparseRepresentationFactory.makeHydrated = function (resource, options) {
var _a, _b, _c, _d, e_1, _e;
var _this = this;
var sparseType = (_a = __assign({}, options), _b = _a.sparseType, _b === void 0 ? 'singleton' : _b), status = (_c = _a.status, _c === void 0 ? status_1.Status.hydrated : _c), eTag = (_d = _a.eTag, _d === void 0 ? undefined : _d);
if (sparseType === 'feed') {
throw new Error('Feed type not implemented. Sparse representation must be singleton or collection');
}
/*
* The passed in resource cannot be mutated because in libraries like Vue2 the bindings will be lost
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource[types_1.state] = new state_1.State(status, eTag);
if ((0, instanceOfCollection_1.instanceOfCollection)(resource)) {
// collection requires feed items to be sparsely populated
// should be able to know from feedOnly state
// TODO: need to know this coming out of load
if ((0, instanceOfFeed_1.instanceOfFeed)(resource)) {
// make collection items
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource.items = resource
.items
.map(function (x) { return _this.makeSparse(__assign(__assign({}, options), { sparseType: 'feed', feedItem: x })); });
}
else {
try {
/*
* Case where the resources already exist and state is to be added after it has been added in-memory
*/
for (var _f = __values(resource.items), _g = _f.next(); !_g.done; _g = _f.next()) {
var item = _g.value;
if ((0, semantic_link_1.instanceOfLinkedRepresentation)(resource) && !(0, instanceOfTrackedRepresentation_1.instanceOfTrackedRepresentation)(resource)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
item[types_1.state] = new state_1.State(status);
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_g && !_g.done && (_e = _f.return)) _e.call(_f);
}
finally { if (e_1) throw e_1.error; }
}
}
} // else singleton
return resource;
};
/**
* The resource is to be made as a sparse resource as there is no data provided via
* the {@link ResourceFactoryOptions.addStateOn} property.
*/
SparseRepresentationFactory.makeSparse = function (options) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
var _this = this;
var uri = (_a = __assign({}, options), _b = _a.uri, _b === void 0 ? '' : _b), // rather than populate with undefined, default to empty string (unclear why this is a good idea)
title = (_c = _a.title, _c === void 0 ? undefined : _c), updated = (_d = _a.updated, _d === void 0 ? undefined : _d), eTag = (_e = _a.eTag, _e === void 0 ? undefined : _e), status = (_f = _a.status, _f === void 0 ? (options === null || options === void 0 ? void 0 : options.uri) ? status_1.Status.locationOnly : status_1.Status.virtual : _f), mappedTitle = (_g = _a.mappedTitle, _g === void 0 ? this.defaultMappedTitleAttributeName : _g), mappedTitleFrom = (_h = _a.mappedTitleFrom, _h === void 0 ? this.defaultMappedFromFeedItemFieldName : _h), mappedUpdated = (_j = _a.mappedUpdated, _j === void 0 ? this.defaultMappedUpdatedAttributeName : _j), mappedUpdatedFrom = (_k = _a.mappedUpdatedFrom, _k === void 0 ? this.defaultMappedFromFeedItemUpdatedFieldName : _k), mappedETagFrom = (_l = _a.mappedETagFrom, _l === void 0 ? this.defaultMappedFromFeedItemETagFieldName : _l), sparseType = (_m = _a.sparseType, _m === void 0 ? 'singleton' : _m);
var sparseResource = (_o = {},
_o[types_1.state] = new state_1.State(status, eTag, updated),
_o.links = [{
rel: linkRelation_1.LinkRelation.Self,
href: uri,
}],
_o);
if (sparseType === 'singleton') {
// feed items come back in on a singleton and have the 'title' mapped to an attribute
// note: 'name' isn't likely to be configured but could be (it also could be injected from global configuration)
return __assign(__assign(__assign({}, sparseResource), (title && (_p = {}, _p[mappedTitle] = title, _p))), (updated && (_q = {}, _q[mappedUpdated] = updated, _q)));
}
else if (sparseType === 'collection') {
var defaultItems = (_r = __assign({}, options).defaultItems, _r === void 0 ? [] : _r);
var items = defaultItems.map(function (item) {
if (typeof item === 'string' /* Uri */) {
return _this.makeSparse({ uri: item });
}
else {
return _this.makeSparse({
uri: item.id,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: item[mappedTitleFrom],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
updated: item[mappedUpdatedFrom],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
eTag: item[mappedETagFrom],
});
}
});
return __assign(__assign({}, sparseResource), { items: items });
}
else if (sparseType === 'feed') /* feedItem */ {
// note: sparseType: 'feed' is an internal type generated from {@link makeHydrated} to populate items
var feedItem = __assign({}, options).feedItem;
if (feedItem) {
return this.makeSparse({
uri: feedItem.id,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: feedItem[mappedTitleFrom],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
updated: feedItem[mappedUpdatedFrom],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
eTag: feedItem[mappedETagFrom],
});
}
else {
log.error('Cannot create resource of type \'feedItem\' should be set - returning unknown');
return this.makeSparse({ status: status_1.Status.unknown });
}
}
else {
log.error('Unsupported type %s', sparseType);
return this.makeSparse({ status: status_1.Status.unknown });
}
};
/**
* Make a collection (that is not pools) from members that are pools.
*/
SparseRepresentationFactory.makeHydratedPoolCollection = function (resource, pool, options) {
var _a, _b, _c, _d, _e;
var _this = this;
var sparseType = (_a = __assign({}, options), _b = _a.sparseType, _b === void 0 ? 'singleton' : _b), status = (_c = _a.status, _c === void 0 ? status_1.Status.hydrated : _c), eTag = (_d = _a.eTag, _d === void 0 ? undefined : _d);
if (sparseType === 'feed') {
throw new Error('Feed type not implemented. Sparse representation must be singleton or collection');
}
// make up a tracked resource for both singleton and collection (and forms)
// this will include links
var tracked = __assign(__assign({}, resource), (_e = {}, _e[types_1.state] = new state_1.State(status, eTag /* currently no last modified passed through */), _e));
if ((0, instanceOfCollection_1.instanceOfCollection)(resource)) {
// collection requires feed items to be sparsely populated
// should be able to know from feedOnly state
var items = this.onAsFeedRepresentation(resource)
.items
.map(function (x) { return _this.makePooledFeedItemResource(pool, x, options); });
// Make the collection, with the pooled items
return __assign(__assign({}, tracked), { items: __spreadArray([], __read(items), false) });
}
else { // the resource is a singleton (or a feed, ...)
// make a singleton (or form)
return tracked;
}
};
/**
* Make an item for a collection using a pool as the source for items.
*/
SparseRepresentationFactory.makePooledFeedItemResource = function (pool, item, options) {
var _a, _b, _c, _d;
var firstMatchingItem = this.firstMatchingFeedItem(pool, item.id);
// when found by id merge across the title and the etag
// which will then make it stale, etc
if (firstMatchingItem) {
var mappedTitleFrom = (_a = __assign({}, options), _b = _a.mappedTitleFrom, _b === void 0 ? this.defaultMappedFromFeedItemFieldName : _b), mappedETagFrom = (_c = _a.mappedETagFrom, _c === void 0 ? this.defaultMappedFromFeedItemETagFieldName : _c), mappedUpdatedFrom = (_d = _a.mappedUpdatedFrom, _d === void 0 ? this.defaultMappedFromFeedItemUpdatedFieldName : _d);
var etag = item[mappedETagFrom];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
var title = item[mappedTitleFrom];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
var lastModified = item[mappedUpdatedFrom];
// const resourceFactoryOptions = { title, eTag: etag } as ResourceFactoryOptions;
return this.mergeFeedItem(firstMatchingItem, __assign(__assign({}, options), { title: title, eTag: etag, lastModified: lastModified })); // item from the pool
}
else {
var newItem = this.makeSparse(__assign(__assign({}, options), { sparseType: 'feed', feedItem: item }));
pool.items.unshift(newItem); // put the item at the beginning of the pool
return newItem;
}
};
/**
* if an incoming feed item is already in the collection AND the incoming has the eTag as part of the feed
* then check if the existing item is stale (ie stale if eTags don't match and requires fetch across the wire)
*
* note: combined with {@link includeItems} items with eTag changes should be refreshed
*/
SparseRepresentationFactory.mergeFeedItemETag = function (resource, options) {
var _a;
var eTag = (_a = __assign({}, options), _a.eTag), lastModified = _a.lastModified;
if ((0, instanceOfTrackedRepresentation_1.instanceOfTrackedRepresentation)(resource)) {
// factory passed in eTag on newly created resources
if (eTag) {
var previousFeedETag = trackedRepresentationUtil_1.TrackedRepresentationUtil.getFeedETag(resource);
if (previousFeedETag !== eTag) {
var state_2 = trackedRepresentationUtil_1.TrackedRepresentationUtil.getState(resource);
state_2.previousStatus = state_2.status;
state_2.status = status_1.Status.staleFromETag;
// update the feed item eTag with the new incoming—which mostly is the same but can be updated
trackedRepresentationUtil_1.TrackedRepresentationUtil.setFeedETag(resource, eTag, lastModified);
}
// look inside the resource that may have been already hydrated and has an eTag value in the headers
}
else if (trackedRepresentationUtil_1.TrackedRepresentationUtil.hasStaleFeedETag(resource)) {
var state_3 = trackedRepresentationUtil_1.TrackedRepresentationUtil.getState(resource);
state_3.previousStatus = state_3.status;
state_3.status = status_1.Status.staleFromETag;
} // else eTags match, don't update
}
else {
log.error('Matched feed item in collection should already be a tracked resource. Developer error');
}
return resource;
};
/**
* The resource is to be made as a sparse resource as there is no data provided via
* the {@link ResourceFactoryOptions.addStateOn} property. Iff a link has been provided
* can the item be fetched from or stored into the pool.
*/
SparseRepresentationFactory.makeSparsePooled = function (pool, options) {
var uri = __assign({}, options).uri;
if (uri) {
var firstMatchingItem = this.firstMatchingFeedItem(pool, uri);
if (firstMatchingItem) {
return this.mergeFeedItem(firstMatchingItem, options); // item from the pool
}
else {
var sparse = this.makeSparse(options);
pool.items.unshift(sparse); // add item to the pool
return sparse;
}
}
else {
// the URI is not known, so return a 'new' sparse item and do not store it in the pool
return this.makeSparse(options); // not eligible for the pool
}
};
/**
* Make a collection (that is not pools) from members that are pools.
*/
SparseRepresentationFactory.makeHydratedPoolSingleton = function (resource, pool, options) {
var uri = semantic_link_1.LinkUtil.getUri(resource, canonicalOrSelf_1.CanonicalOrSelf);
if (uri) {
var firstMatchingItem = this.firstMatchingFeedItem(pool, uri);
if (firstMatchingItem) {
return this.mergeFeedItem(firstMatchingItem, options); // item from the pool
}
else {
var hydrated = this.makeHydrated(resource, options);
pool.items.unshift(hydrated); // add item to the pool
return hydrated;
}
}
else {
return this.makeHydrated(resource, options);
}
};
SparseRepresentationFactory.mergeFeedItem = function (resource, options) {
// incoming changes are merged onto the existing: name, title and eTags (which change state)
this.mergeFeedItemFields(resource, options);
return this.mergeFeedItemETag(resource, options);
};
/**
* Any resource requires incoming sparse representation options to be merged into existing resources. This method
* will follow any options override mappings
*
* Note: in practice, this means that incoming feed will be mapped back onto the UI with new feed titles/updatedAt
*/
SparseRepresentationFactory.mergeFeedItemFields = function (resource, options) {
var _a, _b, _c, _d, _e;
var title = (_a = __assign({}, options), _b = _a.title, _b === void 0 ? undefined : _b), updated = (_c = _a.updated, _c === void 0 ? undefined : _c), mappedTitle = (_d = _a.mappedTitle, _d === void 0 ? this.defaultMappedTitleAttributeName : _d), mappedUpdated = (_e = _a.mappedUpdated, _e === void 0 ? this.defaultMappedUpdatedAttributeName : _e);
if (title) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource[mappedTitle] = title;
}
if (updated) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource[mappedUpdated] = updated;
}
return resource;
};
/**
* Get the {@link ResourceFactoryOptions.addStateOn} data as a {@link FeedRepresentation}.
*/
SparseRepresentationFactory.onAsFeedRepresentation = function (resource) {
if ((0, instanceOfFeed_1.instanceOfFeed)(resource)) {
return resource;
}
else {
log.warn('Resource does not look like a feed');
return resource; // return it anyway.
}
};
SparseRepresentationFactory.defaultMappedTitleAttributeName = 'name';
SparseRepresentationFactory.defaultMappedFromFeedItemFieldName = 'title';
SparseRepresentationFactory.defaultMappedUpdatedAttributeName = 'updatedAt';
SparseRepresentationFactory.defaultMappedFromFeedItemUpdatedFieldName = 'updated';
SparseRepresentationFactory.defaultMappedFromFeedItemETagFieldName = 'eTag';
return SparseRepresentationFactory;
}());
exports.defaultMakeStrategy = SparseRepresentationFactory.defaultMakeStrategy;
exports.pooledCollectionMakeStrategy = SparseRepresentationFactory.pooledCollectionMakeStrategy;
exports.pooledSingletonMakeStrategy = SparseRepresentationFactory.pooledSingletonMakeStrategy;
//# sourceMappingURL=sparseRepresentationFactory.js.map