"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } require("array.prototype.find"); var _rx = require("rx"); var _rx2 = _interopRequireDefault(_rx); var _ajaxAdapter = require("./ajax-adapter"); var _ajaxAdapter2 = _interopRequireDefault(_ajaxAdapter); var Store = (function () { _createClass(Store, null, [{ key: "attr", /** * 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. */ value: function attr(name, options) { if (name && typeof name === 'object') { return Store.attr(null, name); } else { return { type: "attr", "default": options && options["default"], deserialize: function deserialize(data, key) { return data.attributes && data.attributes[name || key]; }, serialize: function serialize(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. */ }, { key: "hasOne", value: function hasOne(name, options) { if (name && typeof name === 'object') { return Store.hasOne(null, name); } else { return { type: "has-one", inverse: options && options.inverse, deserialize: function deserialize(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. */ }, { key: "hasMany", value: function 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 deserialize(data, key) { var _this = this; 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(function (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(function (x) { return { type: x.type, id: x.id }; }) }; } } }; } } }]); function Store(adapter) { _classCallCheck(this, Store); this._adapter = adapter; this._data = {}; this._subject = new _rx2["default"].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: * *
* { name: string, type: string, id: string, resource: object }
*
*
* 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
*/
_createClass(Store, [{
key: "add",
value: function add(object) {
var _this2 = this;
if (object) {
if (object.type && object.id) {
(function () {
var name = _this2._data[object.type] && _this2._data[object.type][object.id] ? "updated" : "added";
var resource = _this2.find(object.type, object.id);
var definition = _this2._types[object.type];
Object.keys(definition).forEach(function (fieldName) {
if (fieldName[0] !== "_") {
_this2._addField(object, resource, definition, fieldName);
}
});
_this2._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.
*/
}, {
key: "convert",
value: function convert(type, id, partial) {
var _this3 = this;
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 {
var _ret2 = (function () {
var data = {
type: type,
attributes: {},
relationships: {}
};
if (id) {
data.id = id;
}
var definition = _this3._types[data.type];
Object.keys(definition).forEach(function (fieldName) {
if (fieldName[0] !== "_") {
definition[fieldName].serialize(partial, data, fieldName);
}
});
return {
v: data
};
})();
if (typeof _ret2 === "object") return _ret2.v;
}
}
/**
* 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);
* });
*/
}, {
key: "create",
value: function 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.
*/
}, {
key: "define",
value: function define(names, definition) {
var _this4 = this;
names = names.constructor === Array ? names : [names];
if (definition) {
definition._names = names;
names.forEach(function (name) {
if (!_this4._types[name]) {
_this4._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!");
* });
*/
}, {
key: "destroy",
value: function 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.
*/
}, {
key: "find",
value: function find(type, id) {
var _this5 = this;
if (type) {
var _ret3 = (function () {
var definition = _this5._types[type];
if (definition) {
if (!_this5._data[type]) {
(function () {
var collection = {};
definition._names.forEach(function (t) {
return _this5._data[t] = collection;
});
})();
}
if (id) {
if (!_this5._data[type][id]) {
_this5._data[type][id] = {
_dependents: [],
type: type,
id: id
};
Object.keys(definition).forEach(function (key) {
if (key[0] !== "_") {
_this5._data[type][id][key] = definition[key]["default"];
}
});
}
return {
v: _this5._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 {
v: _this5.findAll(type)
};
}
} else {
throw new TypeError("Unknown type '" + type + "'");
}
})();
if (typeof _ret3 === "object") return _ret3.v;
} 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.
*/
}, {
key: "findAll",
value: function findAll(type) {
var _this6 = this;
if (type) {
var definition = this._types[type];
if (definition) {
if (!this._data[type]) {
(function () {
var collection = {};
definition._names.forEach(function (t) {
return _this6._data[t] = collection;
});
})();
}
return Object.keys(this._data[type]).map(function (x) {
return _this6._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);
* });
*/
}, {
key: "load",
value: function 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);
* });
*/
}, {
key: "loadAll",
value: function 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 store.observable 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().
*/
}, {
key: "off",
value: function 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 store.observable 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.
*/
}, {
key: "on",
value: function on(event, type, id, callback, context) {
var _this7 = this;
/*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 || "*"]) {
var subscription = this._subject.filter(function (e) {
return e.name === event;
});
subscription = subscription.filter(function (e) {
return _this7._types[type]._names.indexOf(e.type) !== -1;
});
if (id) {
subscription = subscription.filter(function (e) {
return e.id === id;
});
}
subscription = subscription.map(function (e) {
return _this7.find(e.type, e.id);
});
this._subscriptions[event] = this._subscriptions[event] || {};
if (!this._subscriptions[event][type]) {
(function () {
var obj = {};
_this7._types[type]._names.forEach(function (type) {
_this7._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
*/
}, {
key: "push",
value: function push(root) {
var _this8 = this;
if (root.data.constructor === Array) {
root.data.forEach(function (x) {
return _this8.add(x);
});
} else {
this.add(root.data);
}
if (root.included) {
root.included.forEach(function (x) {
return _this8.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.
*/
}, {
key: "remove",
value: function remove(type, id) {
var _this9 = this;
if (type) {
if (this._types[type]) {
if (id) {
var 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(function (id) {
return _this9.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);
* });
*/
}, {
key: "update",
value: function 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);`");
}
}
}, {
key: "_addField",
value: function _addField(object, resource, definition, fieldName) {
var _this10 = this;
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(function (r) {
if (resource[fieldName].indexOf(r) !== -1) {
_this10._removeInverseRelationship(resource, fieldName, r, field);
}
});
newValue.forEach(function (r) {
_this10._addInverseRelationship(resource, fieldName, r, field);
});
}
resource[fieldName] = newValue;
}
}
}, {
key: "_addInverseRelationship",
value: function _addInverseRelationship(sourceResource, sourceFieldName, targetResource, sourceField) {
var targetDefinition = this._types[targetResource.type];
var sourceDefinition = this._types[sourceResource.type];
if (targetDefinition) {
var targetFieldName = [sourceField.inverse].concat(sourceDefinition._names).find(function (x) {
return targetDefinition[x];
});
var 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 + "')");
}
}
}
}, {
key: "_remove",
value: function _remove(resource) {
var _this11 = this;
resource._dependents.forEach(function (dependent) {
var dependentResource = _this11._data[dependent.type][dependent.id];
switch (_this11._types[dependent.type][dependent.fieldName].type) {
case "has-one":
{
dependentResource[dependent.fieldName] = null;
break;
}
case "has-many":
{
var 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(function (d) {
return !(d.type === resource.type && d.id === resource.id);
});
});
delete this._data[resource.type][resource.id];
}
}, {
key: "_removeInverseRelationship",
value: function _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(function (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(function (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(function (r) {
return !(r.type === targetResource.type && r.id === targetResource.id && r.fieldName === targetFieldName);
});
targetResource[targetFieldName] = targetResource[targetFieldName].filter(function (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 + "')");
}
}
}]);
return Store;
})();
exports["default"] = Store;
Store.Rx = _rx2["default"];
Store.AjaxAdapter = _ajaxAdapter2["default"];
module.exports = exports["default"];