Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/haydn/json-api-store. An isomorphic JavaScript library that acts as an in memory data store for JSON API data. particlesystem.com/json-api-store
json-api
Something went wrong. Try again.
28 kB · 776 lines
HTML
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777<!DOCTYPE html><html><head> <meta charset="utf-8"> <base data-ice="baseUrl" href="../../"> <title data-ice="title">src/store.js | API Document</title> <link type="text/css" rel="stylesheet" href="css/style.css"> <link type="text/css" rel="stylesheet" href="css/prettify-tomorrow.css"> <script src="script/prettify/prettify.js"></script> </head><body class="layout-container">
<header> <a href="./">Home</a> <a href="identifiers.html">Identifier</a> <a href="source.html">Source</a> <a data-ice="repoURL" href="https://github.com/haydn/json-api-store" class="repo-url-github">Repository</a> <div class="search-box"> <span> <img src="./image/search.png"> <span class="search-input-edge"></span><input class="search-input"><span class="search-input-edge"></span> </span> <ul class="search-result"></ul> </div></header>
<nav class="navigation" data-ice="nav"><div data-ice="classWrap"> <h2>Class</h2> <ul> <li data-ice="classDoc"><span><a href="class/src/ajax-adapter.js~AjaxAdapter.html">AjaxAdapter</a></span></li><li data-ice="classDoc"><span><a href="class/src/store.js~Store.html">Store</a></span></li></ul></div>
<div data-ice="functionWrap"> <h2><a href="function/">Function</a></h2> <ul> <li data-ice="functionDoc"><span><a href="function/index.html#static-function-ajax">ajax</a></span></li></ul></div>
</nav>
<div class="content" data-ice="content"><h1 data-ice="title">src/store.js</h1><pre class="source-code line-number raw-source-code"><code class="prettyprint linenums" data-ice="content">import "array.prototype.find";import Rx from "rx";import AjaxAdapter from "./ajax-adapter";
export default class Store {
/** * Creates a field definition for an attribute. * * @since 0.1.0 * @param {string} [name] - Name of the property to map this field from. * @param {Object} [options] - An options object. * @param {string} [options.default] - Default value for this field. * @return {Object} - Field definition. */ static attr(name, options) { if (name && typeof name === 'object') { return Store.attr(null, name); } else { return { type: "attr", default: options && options.default, deserialize: function (data, key) { return data.attributes && data.attributes[name || key]; }, serialize: function (resource, data, key) { data.attributes[name || key] = resource[key]; } }; } }
/** * Creates a field definition for an has-one relationship. * * @since 0.1.0 * @param {string} [name] - Name of the property to map this field from. * @param {Object} [options] - An options object. * @param {string} [options.inverse] - Name of the inverse relationship. * @return {Object} - Field definition. */ static hasOne(name, options) { if (name && typeof name === 'object') { return Store.hasOne(null, name); } else { return { type: "has-one", inverse: options && options.inverse, deserialize: function (data, key) { name = name || key; if (data.relationships && data.relationships[name]) { if (data.relationships[name].data === null) { return null; } else if (data.relationships[name].data) { return this.find(data.relationships[name].data.type, data.relationships[name].data.id); } } }, serialize: function serialize(resource, data, key) { if (resource[key] === null) { data.relationships[name || key] = null; } else if (resource[key]) { data.relationships[name || key] = { data: { type: resource[key].type, id: resource[key].id } }; } } }; } }
/** * Creates a field definition for an has-many relationship. * * @since 0.1.0 * @param {string} [name] - Name of the property to map this field from. * @param {Object} [options] - An options object. * @param {string} [options.inverse] - Name of the inverse relationship. * @return {Object} - Field definition. */ static hasMany(name, options) { if (name && typeof name === 'object') { return Store.hasMany(null, name); } else { return { type: "has-many", default: [], inverse: options && options.inverse, deserialize: function (data, key) { name = name || key; if (data.relationships && data.relationships[name]) { if (data.relationships[name].data === null) { return []; } else if (data.relationships[name].data) { return data.relationships[name].data.map((c) => { return this.find(c.type, c.id); }); } } }, serialize: function serialize(resource, data, key) { if (resource[key]) { data.relationships[name || key] = { data: resource[key].map(x => { return { type: x.type, id: x.id }; }) }; } } }; } }
constructor(adapter) {
this._adapter = adapter; this._data = {}; this._subject = new Rx.Subject(); this._subscriptions = {}; this._types = {};
/** * An observable that will emit events when any resource in added, updated * or removed. The object passed to listeners will be in this format: * * <p><pre class="source-code"> * { name: string, type: string, id: string, resource: object } * </pre></p> * * You can learn more about RxJS observables at the GitHub repo: * https://github.com/Reactive-Extensions/RxJS * * @type {Rx.Observable} * @since 0.6.0 * * @example * let store = new Store(); * * store.observable.filter(e => e.name === "added").subscribe(event => { * console.log(event.name); // "added" * console.log(event.type); // "products" * console.log(event.id); // "1" * console.log(event.resource); // Map {...} * }); * * store.observable.filter(e => e.name === "updated").subscribe(event => { * console.log(event.name); // "updated" * console.log(event.type); // "products" * console.log(event.id); // "1" * console.log(event.resource); // Map {...} * }); * * store.observable.filter(e => e.name === "removed").subscribe(event => { * console.log(event.name); // "removed" * console.log(event.type); // "products" * console.log(event.id); // "1" * console.log(event.resource); // null * }); */ this.observable = this._subject.asObservable();
}
/** * Add an individual resource to the store. This is used internally by the * `push()` method. * * @since 0.1.0 * @param {!Object} object - A JSON API Resource Object to be added. See: http://jsonapi.org/format/#document-resource-objects */ add(object) { if (object) { if (object.type && object.id) { let name = this._data[object.type] && this._data[object.type][object.id] ? "updated" : "added"; let resource = this.find(object.type, object.id); let definition = this._types[object.type]; Object.keys(definition).forEach(fieldName => { if (fieldName[0] !== "_") { this._addField(object, resource, definition, fieldName); } }); this._subject.onNext({ name: name, type: object.type, id: object.id, resource: resource }); } else { throw new TypeError(`The data must have a type and id`); } } else { throw new TypeError(`You must provide data to add`); } }
/** * Converts the given partial into a JSON API compliant representation. * * @since 0.5.0 * @param {!string} [type] - The type of the resource. This can be omitted if the partial includes a type property. * @param {!string} [id] - The id of the resource. This can be omitted if the partial includes an id property. * @param {!object} partial - The data to convert. * @return {object} - JSON API version of the object. */ convert(type, id, partial) { if (type && typeof type === "object") { return this.convert(type.type, type.id, type); } else if (id && typeof id === "object") { return this.convert(type, id.id, id); } else { let data = { type: type, attributes: {}, relationships: {} }; if (id) { data.id = id; } let definition = this._types[data.type]; Object.keys(definition).forEach(fieldName => { if (fieldName[0] !== "_") { definition[fieldName].serialize(partial, data, fieldName); } }); return data; } }
/** * Attempts to create the resource through the adapter and adds it to the * store if successful. * * @since 0.5.0 * @param {!string} type - Type of resource. * @param {!Object} partial - Data to create the resource with. * @param {Object} [options] - Options to pass to the adapter. * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); * store.create("product", { title: "A Book" }).subscribe((product) => { * console.log(product.title); * }); */ create(type, partial, options) { if (this._adapter) { return this._adapter.create(this, type, partial, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } }
/** * Defines a type of resource. * * @since 0.2.0 * @param {!string|string[]} names - Name(s) of the resource. * @param {!Object} definition - The resource's definition. */ define(names, definition) { names = (names.constructor === Array) ? names : [ names ]; if (definition) { definition._names = names; names.forEach(name => { if (!this._types[name]) { this._types[name] = definition; } else { throw new Error(`The type '${name}' has already been defined.`); } }); } else { throw new Error(`You must provide a definition for the type '${names[0]}'.`); } }
/** * Attempts to delete the resource through the adapter and removes it from * the store if successful. * * @since 0.5.0 * @param {!string} type - Type of resource. * @param {!string} id - ID of resource. * @param {Object} [options] - Options to pass to the adapter. * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); * store.destroy("product", "1").subscribe(() => { * console.log("Destroyed!"); * }); */ destroy(type, id, options) { if (this._adapter) { return this._adapter.destroy(this, type, id, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } }
/** * Finds a resource by type and id. * * NOTE: If the resource hasn't been loaded via an add() or push() call it * will be automatically created when find is called. * * @since 0.1.0 * @param {!string} type - Type of the resource to find. * @param {!string} id - The id of the resource to find. * @return {Object} - The resource. */ find(type, id) { if (type) { let definition = this._types[type]; if (definition) { if (!this._data[type]) { let collection = {}; definition._names.forEach(t => this._data[t] = collection); } if (id) { if (!this._data[type][id]) { this._data[type][id] = { _dependents: [], type: type, id: id }; Object.keys(definition).forEach(key => { if (key[0] !== "_") { this._data[type][id][key] = definition[key].default; } }); } return this._data[type][id]; } else { // throw new TypeError(`You must provide an id`); /*eslint-disable*/ console.warn([ "Using the `store.find()` method to find an entire collection has been deprecated in favour of `store.findAll()`.", "For more information see: https://github.com/haydn/json-api-store/releases/tag/v0.7.0" ].join("\n")); /*eslint-enable*/ return this.findAll(type); } } else { throw new TypeError(`Unknown type '${type}'`); } } else { throw new TypeError(`You must provide a type`); } }
/** * Finds all the resources of a given type. * * @since 0.7.0 * @param {!string} type - Type of the resource to find. * @return {Object[]} - An array of resources. */ findAll(type) { if (type) { let definition = this._types[type]; if (definition) { if (!this._data[type]) { let collection = {}; definition._names.forEach(t => this._data[t] = collection); } return Object.keys(this._data[type]).map(x => this._data[type][x]); } else { throw new TypeError(`Unknown type '${type}'`); } } else { throw new TypeError(`You must provide a type`); } }
/** * Attempts to load the given resource through the adapter and adds it to the * store if successful. * * @since 0.5.0 * @param {!string} type - Type of resource. * @param {!string} id - ID of resource. * @param {Object} [options] - Options to pass to the adapter. * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); * store.load("products", "1").subscribe((product) => { * console.log(product.title); * }); */ load(type, id, options) { if (!id || typeof id === "object") { /*eslint-disable*/ console.warn([ "Using the `store.load()` method to load an entire collection has been deprecated in favour of `store.loadAll()`.", "For more information see: https://github.com/haydn/json-api-store/releases/tag/v0.7.0" ].join("\n")); /*eslint-enable*/ } if (this._adapter) { return this._adapter.load(this, type, id, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } }
/** * Attempts to load all the resources of the given type through the adapter * and adds them to the store if successful. * * @since 0.7.0 * @param {!string} type - Type of resource. * @param {Object} [options] - Options to pass to the adapter. * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); * store.loadAll("products").subscribe((products) => { * console.log(products); * }); */ loadAll(type, options) { if (this._adapter) { return this._adapter.load(this, type, null, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } }
/** * Unregister an event listener that was registered with on(). * * @deprecated Use the <code>store.observable</code> property instead of this. * @since 0.4.0 * @param {string} event - Name of the event. * @param {string} type - Name of resource to originally passed to on(). * @param {string} [id] - ID of the resource to originally passed to on(). * @param {function} callback - Function originally passed to on(). */ off(event, type, id, callback) { /*eslint-disable*/ console.warn([ "The `store.off()` method has been deprecated in favour of `store.observable`.", "For more information see: https://github.com/haydn/json-api-store/releases/tag/v0.6.0" ].join("\n")); /*eslint-enable*/ if (event === "added" || event === "updated" || event === "removed") { if (this._types[type]) { if (id && ({}).toString.call(id) === '[object Function]') { this.off.call(this, event, type, null, id, callback); } else if (this._subscriptions[event] && this._subscriptions[event][type] && this._subscriptions[event][type][id || "*"]) { this._subscriptions[event][type][id || "*"].dispose(); delete this._subscriptions[event][type][id || "*"]; } } else { throw new Error(`Unknown type '${type}'`); } } else { throw new Error(`Unknown event '${event}'`); } }
/** * Register an event listener: "added", "updated" or "removed". * * @deprecated Use the <code>store.observable</code> property instead of this. * @since 0.4.0 * @param {string} event - Name of the event. * @param {string} type - Name of resource to watch. * @param {string} [id] - ID of the resource to watch. * @param {function} callback - Function to call when the event occurs. * @param {Object} [context] - Context in which to call the callback. */ on(event, type, id, callback, context) { /*eslint-disable*/ console.warn([ "The `store.on()` method has been deprecated in favour of `store.observable`.", "For more information see: https://github.com/haydn/json-api-store/releases/tag/v0.6.0" ].join("\n")); /*eslint-enable*/ if (event === "added" || event === "updated" || event === "removed") { if (this._types[type]) { if (id && ({}).toString.call(id) === '[object Function]') { this.on.call(this, event, type, null, id, callback); } else if (!this._subscriptions[event] || !this._subscriptions[event][type] || !this._subscriptions[event][type][id || "*"]) { let subscription = this._subject.filter(e => e.name === event); subscription = subscription.filter(e => this._types[type]._names.indexOf(e.type) !== -1); if (id) { subscription = subscription.filter(e => e.id === id); } subscription = subscription.map(e => this.find(e.type, e.id)); this._subscriptions[event] = this._subscriptions[event] || {}; if (!this._subscriptions[event][type]) { let obj = {}; this._types[type]._names.forEach(type => { this._subscriptions[event][type] = obj; }); } this._subscriptions[event][type][id || "*"] = subscription.subscribe(callback.bind(context)); } } else { throw new Error(`Unknown type '${type}'`); } } else { throw new Error(`Unknown event '${event}'`); } }
/** * Add a JSON API response to the store. This method can be used to handle a * successful GET or POST response from the server. * * @since 0.1.0 * @param {Object} root - Top Level Object to push. See: http://jsonapi.org/format/#document-top-level */ push(root) { if (root.data.constructor === Array) { root.data.forEach(x => this.add(x)); } else { this.add(root.data); } if (root.included) { root.included.forEach(x => this.add(x)); } }
/** * Remove a resource or collection of resources from the store. * * @since 0.1.0 * @param {!string} type - Type of the resource(s) to remove. * @param {string} [id] - The id of the resource to remove. If omitted all * resources of the type will be removed. */ remove(type, id) { if (type) { if (this._types[type]) { if (id) { let resource = this._data[type] && this._data[type][id]; if (resource) { this._remove(resource); this._subject.onNext({ name: "removed", type: type, id: id, resource: null }); } } else { Object.keys(this._data[type]).forEach(id => this.remove(type, id)); } } else { throw new TypeError(`Unknown type '${type}'`); } } else { throw new TypeError(`You must provide a type to remove`); } }
/** * Attempts to update the resource through the adapter and updates it in the * store if successful. * * @since 0.5.0 * @param {!string} type - Type of resource. * @param {!string} id - ID of resource. * @param {!Object} partial - Data to update the resource with. * @param {Object} [options] - Options to pass to the adapter. * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); * store.update("product", "1", { title: "foo" }).subscribe((product) => { * console.log(product.title); * }); */ update(type, id, partial, options) { if (this._adapter) { return this._adapter.update(this, type, id, partial, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } }
_addField(object, resource, definition, fieldName) { var field = definition[fieldName]; var newValue = field.deserialize.call(this, object, fieldName); if (typeof newValue !== "undefined") { if (field.type === "has-one") { if (resource[fieldName]) { this._removeInverseRelationship(resource, fieldName, resource[fieldName], field); } if (newValue) { this._addInverseRelationship(resource, fieldName, newValue, field); } } else if (field.type === "has-many") { resource[fieldName].forEach(r => { if (resource[fieldName].indexOf(r) !== -1) { this._removeInverseRelationship(resource, fieldName, r, field); } }); newValue.forEach(r => { this._addInverseRelationship(resource, fieldName, r, field); }); } resource[fieldName] = newValue; } }
_addInverseRelationship(sourceResource, sourceFieldName, targetResource, sourceField) { var targetDefinition = this._types[targetResource.type]; var sourceDefinition = this._types[sourceResource.type]; if (targetDefinition) { let targetFieldName = [ sourceField.inverse ].concat(sourceDefinition._names).find(x => targetDefinition[x]); let targetField = targetDefinition && targetDefinition[targetFieldName]; targetResource._dependents.push({ type: sourceResource.type, id: sourceResource.id, fieldName: sourceFieldName }); if (targetField) { if (targetField.type === "has-one") { sourceResource._dependents.push({ type: targetResource.type, id: targetResource.id, fieldName: targetFieldName }); targetResource[targetFieldName] = sourceResource; } else if (targetField.type === "has-many") { sourceResource._dependents.push({ type: targetResource.type, id: targetResource.id, fieldName: targetFieldName }); if (targetResource[targetFieldName].indexOf(sourceResource) === -1) { targetResource[targetFieldName].push(sourceResource); } } else if (targetField.type === "attr") { throw new Error(`The the inverse relationship for '${sourceFieldName}' is an attribute ('${targetFieldName}')`); } } else if (sourceField.inverse) { throw new Error(`The the inverse relationship for '${sourceFieldName}' is missing ('${sourceField.inverse}')`); } } }
_remove(resource) { resource._dependents.forEach(dependent => { let dependentResource = this._data[dependent.type][dependent.id]; switch (this._types[dependent.type][dependent.fieldName].type) { case "has-one": { dependentResource[dependent.fieldName] = null; break; } case "has-many": { let index = dependentResource[dependent.fieldName].indexOf(resource); if (index !== -1) { dependentResource[dependent.fieldName].splice(index, 1); } break; } default: { break; } } // TODO: This only needs to be run once for each dependent. dependentResource._dependents = dependentResource._dependents.filter(d => { return !(d.type === resource.type && d.id === resource.id); }); }); delete this._data[resource.type][resource.id]; }
_removeInverseRelationship(sourceResource, sourceFieldName, targetResource, sourceField) { var targetDefinition = this._types[targetResource.type]; var targetFieldName = sourceField.inverse || sourceResource.type; var targetField = targetDefinition && targetDefinition[targetFieldName]; targetResource._dependents = targetResource._dependents.filter(r => { return !(r.type === sourceResource.type && r.id === sourceResource.id && r.fieldName === sourceFieldName); }); if (targetField) { if (targetField.type === "has-one") { sourceResource._dependents = sourceResource._dependents.filter(r => { return !(r.type === targetResource.type && r.id === targetResource.id && r.fieldName === targetFieldName); }); targetResource[targetFieldName] = null; } else if (targetField.type === "has-many") { sourceResource._dependents = sourceResource._dependents.filter(r => { return !(r.type === targetResource.type && r.id === targetResource.id && r.fieldName === targetFieldName); }); targetResource[targetFieldName] = targetResource[targetFieldName].filter(r => { return r !== sourceResource; }); } else if (targetField.type === "attr") { throw new Error(`The the inverse relationship for '${sourceFieldName}' is an attribute ('${targetFieldName}')`); } } else if (sourceField.inverse) { throw new Error(`The the inverse relationship for '${sourceFieldName}' is missing ('${sourceField.inverse}')`); } }
}
Store.Rx = Rx;Store.AjaxAdapter = AjaxAdapter;</code></pre>
</div>
<footer class="footer"> Generated by <a href="https://esdoc.org">ESDoc<span data-ice="esdocVersion">(0.2.6)</span></a></footer>
<script src="script/search_index.js"></script><script src="script/search.js"></script><script src="script/pretty-print.js"></script><script src="script/inherited-summary.js"></script><script src="script/test-summary.js"></script><script src="script/inner-link.js"></script><script src="script/patch-for-local.js"></script></body></html>