UNPKG

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
"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