Comparing version 2.2.5 to 3.0.0
# Changelog | ||
## `3.0.0` | ||
New features 🎩: | ||
- Reimplemented `mustFind` and `mustGet` methods in Collection. | ||
- Brought back the `request` attribute in Model. The attribute tracks the last issued request. | ||
- Brought back `onProgress` hook in `save` method. | ||
Solved bugs 🐛: | ||
- Fix https://github.com/masylum/mobx-rest/issues/47 | ||
- Fix https://github.com/masylum/mobx-rest/issues/41 | ||
What's changed 💅: | ||
- Migrated to Typescript | ||
- Made RPC label optional with a default `fetching` value | ||
Breaking changes ☢️: | ||
- Migrated to Mobx 5+ | ||
- Rollback default `keepChanges` flag value to `false`. | ||
- Compatible adapters need to implement `data` as their second argument [as such](https://github.com/masylum/mobx-rest-jquery-adapter/commit/1e55c15dc37d372db1ae4345dedef855b8fb7611#diff-1fdf421c05c1140f6d71444ea2b27638R135). | ||
## `3.0.0.alpha` | ||
:sparkles: | ||
Full description here: https://github.com/masylum/mobx-rest/pull/39 | ||
Kudos to @rdiazv | ||
## `2.2.5` | ||
@@ -4,0 +30,0 @@ |
821
lib/index.js
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
exports.Request = exports.apiClient = exports.Model = exports.Collection = undefined; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var _Collection = require('./Collection'); | ||
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } | ||
var _Collection2 = _interopRequireDefault(_Collection); | ||
var mobx = require('mobx'); | ||
var lodash = require('lodash'); | ||
var deepmerge = _interopDefault(require('deepmerge')); | ||
var _Model = require('./Model'); | ||
/*! ***************************************************************************** | ||
Copyright (c) Microsoft Corporation. All rights reserved. | ||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use | ||
this file except in compliance with the License. You may obtain a copy of the | ||
License at http://www.apache.org/licenses/LICENSE-2.0 | ||
var _Model2 = _interopRequireDefault(_Model); | ||
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED | ||
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, | ||
MERCHANTABLITY OR NON-INFRINGEMENT. | ||
var _Request = require('./Request'); | ||
See the Apache Version 2.0 License for specific language governing permissions | ||
and limitations under the License. | ||
***************************************************************************** */ | ||
/* global Reflect, Promise */ | ||
var _Request2 = _interopRequireDefault(_Request); | ||
var extendStatics = function(d, b) { | ||
extendStatics = Object.setPrototypeOf || | ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || | ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; | ||
return extendStatics(d, b); | ||
}; | ||
var _apiClient = require('./apiClient'); | ||
function __extends(d, b) { | ||
extendStatics(d, b); | ||
function __() { this.constructor = d; } | ||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); | ||
} | ||
var _apiClient2 = _interopRequireDefault(_apiClient); | ||
var __assign = function() { | ||
__assign = Object.assign || function __assign(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); | ||
}; | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function __rest(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) | ||
t[p[i]] = s[p[i]]; | ||
return t; | ||
} | ||
exports.Collection = _Collection2.default; | ||
exports.Model = _Model2.default; | ||
exports.apiClient = _apiClient2.default; | ||
exports.Request = _Request2.default; | ||
function __decorate(decorators, target, key, desc) { | ||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; | ||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); | ||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; | ||
return c > 3 && r && Object.defineProperty(target, key, r), r; | ||
} | ||
var ErrorObject = /** @class */ (function () { | ||
function ErrorObject(error) { | ||
this.error = null; | ||
this.payload = {}; | ||
this.requestResponse = null; | ||
if (error instanceof Error) { | ||
console.error(error); | ||
this.requestResponse = null; | ||
this.error = error; | ||
} | ||
else if (typeof error === 'string') { | ||
this.requestResponse = null; | ||
this.error = error; | ||
} | ||
else if (error.requestResponse || error.error) { | ||
this.requestResponse = error.requestResponse; | ||
this.error = error.error; | ||
} | ||
else { | ||
this.payload = error; | ||
} | ||
} | ||
return ErrorObject; | ||
}()); | ||
var Request = /** @class */ (function () { | ||
function Request(promise, _a) { | ||
var _this = this; | ||
var _b = _a === void 0 ? {} : _a, labels = _b.labels, abort = _b.abort, _c = _b.progress, progress = _c === void 0 ? 0 : _c; | ||
this.state = 'pending'; | ||
this.labels = labels; | ||
this.abort = abort; | ||
this.progress = progress = 0; | ||
this.promise = promise; | ||
promise | ||
.then(function () { _this.state = 'fulfilled'; }) | ||
.catch(function () { _this.state = 'rejected'; }); | ||
} | ||
// This allows to use async/await on the request object | ||
Request.prototype.then = function (onFulfilled, onRejected) { | ||
return this.promise.then(function (data) { return onFulfilled(data || {}); }, onRejected); | ||
}; | ||
__decorate([ | ||
mobx.observable | ||
], Request.prototype, "progress", void 0); | ||
__decorate([ | ||
mobx.observable | ||
], Request.prototype, "state", void 0); | ||
return Request; | ||
}()); | ||
var currentAdapter; | ||
/** | ||
* Sets or gets the api client instance | ||
*/ | ||
function apiClient(adapter, options) { | ||
if (options === void 0) { options = {}; } | ||
if (adapter) { | ||
currentAdapter = Object.assign({}, adapter, options); | ||
} | ||
if (!currentAdapter) { | ||
throw new Error('You must set an adapter first!'); | ||
} | ||
return currentAdapter; | ||
} | ||
var Base = /** @class */ (function () { | ||
function Base() { | ||
this.requests = mobx.observable.array([]); | ||
} | ||
/** | ||
* Returns the resource's url. | ||
* | ||
* @abstract | ||
*/ | ||
Base.prototype.url = function () { | ||
throw new Error('You must implement this method'); | ||
}; | ||
Base.prototype.withRequest = function (labels, promise, abort) { | ||
var _this = this; | ||
if (typeof labels === 'string') { | ||
labels = [labels]; | ||
} | ||
var handledPromise = promise | ||
.then(function (response) { | ||
_this.requests.remove(request); | ||
return response; | ||
}) | ||
.catch(function (error) { | ||
_this.requests.remove(request); | ||
throw new ErrorObject(error); | ||
}); | ||
var request = new Request(handledPromise, { | ||
labels: labels, | ||
abort: abort | ||
}); | ||
this.requests.push(request); | ||
return request; | ||
}; | ||
Base.prototype.getRequest = function (label) { | ||
return this.requests.find(function (request) { return lodash.includes(request.labels, label); }); | ||
}; | ||
Base.prototype.getAllRequests = function (label) { | ||
return this.requests.filter(function (request) { return lodash.includes(request.labels, label); }); | ||
}; | ||
/** | ||
* Questions whether the request exists | ||
* and matches a certain label | ||
*/ | ||
Base.prototype.isRequest = function (label) { | ||
return !!this.getRequest(label); | ||
}; | ||
/** | ||
* Call an RPC action for all those | ||
* non-REST endpoints that you may have in | ||
* your API. | ||
*/ | ||
// TODO: Type endpoint with string | { rootUrl: string } | ||
Base.prototype.rpc = function (endpoint, options, label) { | ||
if (label === void 0) { label = 'fetching'; } | ||
var url = lodash.isObject(endpoint) ? endpoint.rootUrl : this.url() + "/" + endpoint; | ||
var _a = apiClient().post(url, options), promise = _a.promise, abort = _a.abort; | ||
return this.withRequest(label, promise, abort); | ||
}; | ||
__decorate([ | ||
mobx.observable | ||
], Base.prototype, "request", void 0); | ||
__decorate([ | ||
mobx.observable.shallow | ||
], Base.prototype, "requests", void 0); | ||
__decorate([ | ||
mobx.action | ||
], Base.prototype, "rpc", null); | ||
return Base; | ||
}()); | ||
var dontMergeArrays = function (_oldArray, newArray) { return newArray; }; | ||
var Model = /** @class */ (function (_super) { | ||
__extends(Model, _super); | ||
function Model(attributes, defaultAttributes) { | ||
if (attributes === void 0) { attributes = {}; } | ||
if (defaultAttributes === void 0) { defaultAttributes = {}; } | ||
var _this = _super.call(this) || this; | ||
_this.defaultAttributes = {}; | ||
_this.attributes = mobx.observable.map(); | ||
_this.committedAttributes = mobx.observable.map(); | ||
_this.optimisticId = lodash.uniqueId('i_'); | ||
_this.collection = null; | ||
_this.defaultAttributes = defaultAttributes; | ||
var mergedAttributes = __assign({}, _this.defaultAttributes, attributes); | ||
_this.attributes.replace(mergedAttributes); | ||
_this.commitChanges(); | ||
return _this; | ||
} | ||
/** | ||
* Returns a JSON representation | ||
* of the model | ||
*/ | ||
Model.prototype.toJS = function () { | ||
return mobx.toJS(this.attributes, { exportMapsAsObjects: true }); | ||
}; | ||
Object.defineProperty(Model.prototype, "primaryKey", { | ||
/** | ||
* Determine what attribute do you use | ||
* as a primary id | ||
* | ||
* @abstract | ||
*/ | ||
get: function () { | ||
return 'id'; | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
/** | ||
* Return the base url used in | ||
* the `url` method | ||
* | ||
* @abstract | ||
*/ | ||
Model.prototype.urlRoot = function () { | ||
return null; | ||
}; | ||
/** | ||
* Return the url for this given REST resource | ||
*/ | ||
Model.prototype.url = function () { | ||
var urlRoot = this.urlRoot(); | ||
if (!urlRoot && this.collection) { | ||
urlRoot = this.collection.url(); | ||
} | ||
if (!urlRoot) { | ||
throw new Error('implement `urlRoot` method or `url` on the collection'); | ||
} | ||
if (this.isNew) { | ||
return urlRoot; | ||
} | ||
else { | ||
return urlRoot + "/" + this.get(this.primaryKey); | ||
} | ||
}; | ||
Object.defineProperty(Model.prototype, "isNew", { | ||
/** | ||
* Wether the resource is new or not | ||
* | ||
* We determine this asking if it contains | ||
* the `primaryKey` attribute (set by the server). | ||
*/ | ||
get: function () { | ||
return !this.has(this.primaryKey) || !this.get(this.primaryKey); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
/** | ||
* Get the attribute from the model. | ||
* | ||
* Since we want to be sure changes on | ||
* the schema don't fail silently we | ||
* throw an error if the field does not | ||
* exist. | ||
* | ||
* If you want to deal with flexible schemas | ||
* use `has` to check wether the field | ||
* exists. | ||
*/ | ||
Model.prototype.get = function (attribute) { | ||
if (this.has(attribute)) { | ||
return this.attributes.get(attribute); | ||
} | ||
throw new Error("Attribute \"" + attribute + "\" not found"); | ||
}; | ||
/** | ||
* Returns whether the given field exists | ||
* for the model. | ||
*/ | ||
Model.prototype.has = function (attribute) { | ||
return this.attributes.has(attribute); | ||
}; | ||
Object.defineProperty(Model.prototype, "id", { | ||
/** | ||
* Get an id from the model. It will use either | ||
* the backend assigned one or the client. | ||
*/ | ||
get: function () { | ||
return this.has(this.primaryKey) | ||
? this.get(this.primaryKey) | ||
: this.optimisticId; | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
Object.defineProperty(Model.prototype, "changedAttributes", { | ||
/** | ||
* Get an array with the attributes names that have changed. | ||
*/ | ||
get: function () { | ||
return getChangedAttributesBetween(mobx.toJS(this.committedAttributes), mobx.toJS(this.attributes)); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
Object.defineProperty(Model.prototype, "changes", { | ||
/** | ||
* Gets the current changes. | ||
*/ | ||
get: function () { | ||
return getChangesBetween(mobx.toJS(this.committedAttributes), mobx.toJS(this.attributes)); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
/** | ||
* If an attribute is specified, returns true if it has changes. | ||
* If no attribute is specified, returns true if any attribute has changes. | ||
*/ | ||
Model.prototype.hasChanges = function (attribute) { | ||
if (attribute) { | ||
return lodash.includes(this.changedAttributes, attribute); | ||
} | ||
return this.changedAttributes.length > 0; | ||
}; | ||
Model.prototype.commitChanges = function () { | ||
this.committedAttributes.replace(mobx.toJS(this.attributes)); | ||
}; | ||
Model.prototype.discardChanges = function () { | ||
this.attributes.replace(mobx.toJS(this.committedAttributes)); | ||
}; | ||
/** | ||
* Replace all attributes with new data | ||
*/ | ||
Model.prototype.reset = function (data) { | ||
this.attributes.replace(data | ||
? __assign({}, this.defaultAttributes, data) : this.defaultAttributes); | ||
}; | ||
/** | ||
* Merge the given attributes with | ||
* the current ones | ||
*/ | ||
Model.prototype.set = function (data) { | ||
this.attributes.merge(data); | ||
}; | ||
/** | ||
* Fetches the model from the backend. | ||
*/ | ||
Model.prototype.fetch = function (_a) { | ||
var _this = this; | ||
if (_a === void 0) { _a = {}; } | ||
var data = _a.data, otherOptions = __rest(_a, ["data"]); | ||
var _b = apiClient().get(this.url(), data, otherOptions), abort = _b.abort, promise = _b.promise; | ||
promise | ||
.then(function (data) { | ||
_this.set(data); | ||
_this.commitChanges(); | ||
}); | ||
return this.withRequest('fetching', promise, abort); | ||
}; | ||
/** | ||
* Merges old attributes with new ones. | ||
* By default it doesn't merge arrays. | ||
*/ | ||
Model.prototype.applyPatchChanges = function (oldAttributes, changes) { | ||
return deepmerge(oldAttributes, changes, { | ||
arrayMerge: dontMergeArrays | ||
}); | ||
}; | ||
/** | ||
* Saves the resource on the backend. | ||
* | ||
* If the item has a `primaryKey` it updates it, | ||
* otherwise it creates the new resource. | ||
* | ||
* It supports optimistic and patch updates. | ||
*/ | ||
Model.prototype.save = function (attributes, _a) { | ||
var _this = this; | ||
if (_a === void 0) { _a = {}; } | ||
var _b = _a.optimistic, optimistic = _b === void 0 ? true : _b, _c = _a.patch, patch = _c === void 0 ? false : _c, _d = _a.keepChanges, keepChanges = _d === void 0 ? false : _d, otherOptions = __rest(_a, ["optimistic", "patch", "keepChanges"]); | ||
var currentAttributes = this.toJS(); | ||
var label = this.isNew ? 'creating' : 'updating'; | ||
var data; | ||
if (patch && attributes && !this.isNew) { | ||
data = attributes; | ||
} | ||
else if (patch && !this.isNew) { | ||
data = this.changes; | ||
} | ||
else { | ||
data = __assign({}, currentAttributes, attributes); | ||
} | ||
var method; | ||
if (this.isNew) { | ||
method = 'post'; | ||
} | ||
else if (patch) { | ||
method = 'patch'; | ||
} | ||
else { | ||
method = 'put'; | ||
} | ||
if (optimistic && attributes) { | ||
this.set(patch | ||
? this.applyPatchChanges(currentAttributes, attributes) | ||
: attributes); | ||
} | ||
var onProgress = lodash.debounce(function (progress) { | ||
if (optimistic && _this.request) | ||
_this.request.progress = progress; | ||
}); | ||
var _e = apiClient()[method](this.url(), data, __assign({ onProgress: onProgress }, otherOptions)), promise = _e.promise, abort = _e.abort; | ||
promise | ||
.then(function (data) { | ||
var changes = getChangesBetween(currentAttributes, mobx.toJS(_this.attributes)); | ||
mobx.runInAction('save success', function () { | ||
_this.set(data); | ||
_this.commitChanges(); | ||
if (keepChanges) { | ||
_this.set(_this.applyPatchChanges(data, changes)); | ||
} | ||
}); | ||
}) | ||
.catch(function (error) { | ||
_this.set(currentAttributes); | ||
throw error; | ||
}); | ||
this.request = this.withRequest(['saving', label], promise, abort); | ||
return this.request; | ||
}; | ||
/** | ||
* Destroys the resurce on the client and | ||
* requests the backend to delete it there | ||
* too | ||
*/ | ||
Model.prototype.destroy = function (_a) { | ||
var _this = this; | ||
if (_a === void 0) { _a = {}; } | ||
var data = _a.data, _b = _a.optimistic, optimistic = _b === void 0 ? true : _b, otherOptions = __rest(_a, ["data", "optimistic"]); | ||
var collection = this.collection; | ||
if (this.isNew && collection) { | ||
collection.remove(this); | ||
return new Request(Promise.resolve()); | ||
} | ||
if (this.isNew) { | ||
return new Request(Promise.resolve()); | ||
} | ||
var _c = apiClient().del(this.url(), data, otherOptions), promise = _c.promise, abort = _c.abort; | ||
if (optimistic && collection) { | ||
collection.remove(this); | ||
} | ||
promise | ||
.then(function (data) { | ||
if (!optimistic && collection) | ||
collection.remove(_this); | ||
}) | ||
.catch(function (error) { | ||
if (optimistic && collection) | ||
collection.add(_this); | ||
throw error; | ||
}); | ||
return this.withRequest('destroying', promise, abort); | ||
}; | ||
__decorate([ | ||
mobx.computed | ||
], Model.prototype, "isNew", null); | ||
__decorate([ | ||
mobx.computed | ||
], Model.prototype, "changedAttributes", null); | ||
__decorate([ | ||
mobx.computed | ||
], Model.prototype, "changes", null); | ||
__decorate([ | ||
mobx.action | ||
], Model.prototype, "commitChanges", null); | ||
__decorate([ | ||
mobx.action | ||
], Model.prototype, "discardChanges", null); | ||
__decorate([ | ||
mobx.action | ||
], Model.prototype, "reset", null); | ||
__decorate([ | ||
mobx.action | ||
], Model.prototype, "set", null); | ||
__decorate([ | ||
mobx.action | ||
], Model.prototype, "fetch", null); | ||
__decorate([ | ||
mobx.action | ||
], Model.prototype, "save", null); | ||
__decorate([ | ||
mobx.action | ||
], Model.prototype, "destroy", null); | ||
return Model; | ||
}(Base)); | ||
var getChangedAttributesBetween = function (source, target) { | ||
var keys = lodash.union(Object.keys(source), Object.keys(target)); | ||
return keys.filter(function (key) { return !lodash.isEqual(source[key], target[key]); }); | ||
}; | ||
var getChangesBetween = function (source, target) { | ||
var changes = {}; | ||
getChangedAttributesBetween(source, target).forEach(function (key) { | ||
changes[key] = lodash.isPlainObject(source[key]) && lodash.isPlainObject(target[key]) | ||
? getChangesBetween(source[key], target[key]) | ||
: target[key]; | ||
}); | ||
return changes; | ||
}; | ||
var Collection = /** @class */ (function (_super) { | ||
__extends(Collection, _super); | ||
function Collection(data) { | ||
if (data === void 0) { data = []; } | ||
var _this = _super.call(this) || this; | ||
_this.models = mobx.observable.array([]); | ||
_this.set(data); | ||
return _this; | ||
} | ||
Object.defineProperty(Collection.prototype, "length", { | ||
/** | ||
* Alias for models.length | ||
*/ | ||
get: function () { | ||
return this.models.length; | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
/** | ||
* Alias for models.map | ||
*/ | ||
Collection.prototype.map = function (callback) { | ||
return this.models.map(callback); | ||
}; | ||
/** | ||
* Alias for models.forEach | ||
*/ | ||
Collection.prototype.forEach = function (callback) { | ||
return this.models.forEach(callback); | ||
}; | ||
/** | ||
* Returns the URL where the model's resource would be located on the server. | ||
* | ||
* @abstract | ||
*/ | ||
Collection.prototype.url = function () { | ||
throw new Error('You must implement this method'); | ||
}; | ||
/** | ||
* Returns a JSON representation | ||
* of the collection | ||
*/ | ||
Collection.prototype.toJS = function () { | ||
return this.models.map(function (model) { return model.toJS(); }); | ||
}; | ||
/** | ||
* Alias of slice | ||
*/ | ||
Collection.prototype.toArray = function () { | ||
return this.slice(); | ||
}; | ||
/** | ||
* Returns a defensive shallow array representation | ||
* of the collection | ||
*/ | ||
Collection.prototype.slice = function () { | ||
return this.models.slice(); | ||
}; | ||
Object.defineProperty(Collection.prototype, "isEmpty", { | ||
/** | ||
* Wether the collection is empty | ||
*/ | ||
get: function () { | ||
return this.length === 0; | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
/** | ||
* Gets the ids of all the items in the collection | ||
*/ | ||
Collection.prototype._ids = function () { | ||
return this.models.map(function (item) { return item.id; }).filter(Boolean); | ||
}; | ||
/** | ||
* Get a resource at a given position | ||
*/ | ||
Collection.prototype.at = function (index) { | ||
return this.models[index]; | ||
}; | ||
/** | ||
* Get a resource with the given id or uuid | ||
*/ | ||
Collection.prototype.get = function (id, _a) { | ||
var _b = (_a === void 0 ? {} : _a).required, required = _b === void 0 ? false : _b; | ||
var model = this.models.find(function (item) { return item.id === id; }); | ||
if (!model && required) { | ||
throw Error("Invariant: Model must be found with id: " + id); | ||
} | ||
return model; | ||
}; | ||
/** | ||
* Get a resource with the given id or uuid or fail loudly. | ||
*/ | ||
Collection.prototype.mustGet = function (id) { | ||
return this.get(id, { required: true }); | ||
}; | ||
/** | ||
* Get resources matching criteria | ||
*/ | ||
Collection.prototype.filter = function (query) { | ||
return this.models.filter(function (model) { | ||
return typeof query === 'function' | ||
? query(model) | ||
: lodash.isMatch(model.toJS(), query); | ||
}); | ||
}; | ||
/** | ||
* Finds an element with the given matcher | ||
*/ | ||
Collection.prototype.find = function (query, _a) { | ||
var _b = (_a === void 0 ? {} : _a).required, required = _b === void 0 ? false : _b; | ||
var model = this.models.find(function (model) { | ||
return typeof query === 'function' | ||
? query(model) | ||
: lodash.isMatch(model.toJS(), query); | ||
}); | ||
if (!model && required) { | ||
throw Error("Invariant: Model must be found"); | ||
} | ||
return model; | ||
}; | ||
/** | ||
* Get a resource with the given id or uuid or fails loudly. | ||
*/ | ||
Collection.prototype.mustFind = function (query) { | ||
return this.find(query, { required: true }); | ||
}; | ||
/** | ||
* Adds a model or collection of models. | ||
*/ | ||
Collection.prototype.add = function (data) { | ||
var _this = this; | ||
var _a; | ||
if (!Array.isArray(data)) | ||
data = [data]; | ||
var models = data.map(function (m) { return _this.build(m); }); | ||
(_a = this.models).push.apply(_a, models); | ||
return models; | ||
}; | ||
/** | ||
* Resets the collection of models. | ||
*/ | ||
Collection.prototype.reset = function (data) { | ||
var _this = this; | ||
this.models.replace(data.map(function (m) { return _this.build(m); })); | ||
}; | ||
/** | ||
* Removes the model with the given ids or uuids | ||
*/ | ||
Collection.prototype.remove = function (ids) { | ||
var _this = this; | ||
if (!Array.isArray(ids)) { | ||
ids = [ids]; | ||
} | ||
ids.forEach(function (id) { | ||
var model; | ||
if (id instanceof Model && id.collection === _this) { | ||
model = id; | ||
} | ||
else if (typeof id === 'number') { | ||
model = _this.get(id); | ||
} | ||
if (!model) | ||
return; | ||
_this.models.splice(_this.models.indexOf(model), 1); | ||
model.collection = undefined; | ||
}); | ||
}; | ||
/** | ||
* Sets the resources into the collection. | ||
* | ||
* You can disable adding, changing or removing. | ||
*/ | ||
Collection.prototype.set = function (resources, _a) { | ||
var _this = this; | ||
var _b = _a === void 0 ? {} : _a, _c = _b.add, add = _c === void 0 ? true : _c, _d = _b.change, change = _d === void 0 ? true : _d, _e = _b.remove, remove = _e === void 0 ? true : _e; | ||
if (remove) { | ||
var ids = resources.map(function (r) { return r.id; }); | ||
var toRemove = lodash.difference(this._ids(), ids); | ||
if (toRemove.length) | ||
this.remove(toRemove); | ||
} | ||
resources.forEach(function (resource) { | ||
var model = _this.get(resource.id); | ||
if (model && change) | ||
model.set(resource); | ||
if (!model && add) | ||
_this.add([resource]); | ||
}); | ||
}; | ||
/** | ||
* Creates a new model instance with the given attributes | ||
*/ | ||
Collection.prototype.build = function (attributes) { | ||
if (attributes === void 0) { attributes = {}; } | ||
if (attributes instanceof Model) { | ||
attributes.collection = this; | ||
return attributes; | ||
} | ||
var ModelClass = this.model(attributes); | ||
var model = new ModelClass(attributes); | ||
model.collection = this; | ||
return model; | ||
}; | ||
/** | ||
* Creates the model and saves it on the backend | ||
* | ||
* The default behaviour is optimistic but this | ||
* can be tuned. | ||
*/ | ||
Collection.prototype.create = function (attributesOrModel, _a) { | ||
var _this = this; | ||
var _b = (_a === void 0 ? {} : _a).optimistic, optimistic = _b === void 0 ? true : _b; | ||
var model = this.build(attributesOrModel); | ||
var _c = model.save(), abort = _c.abort, promise = _c.promise; | ||
if (optimistic) { | ||
this.add(model); | ||
} | ||
promise | ||
.then(function (response) { | ||
if (!optimistic) | ||
_this.add(model); | ||
}) | ||
.catch(function (error) { | ||
if (optimistic) | ||
_this.remove(model); | ||
throw error; | ||
}); | ||
return this.withRequest('creating', promise, abort); | ||
}; | ||
/** | ||
* Fetches the models from the backend. | ||
* | ||
* It uses `set` internally so you can | ||
* use the options to disable adding, changing | ||
* or removing. | ||
*/ | ||
Collection.prototype.fetch = function (_a) { | ||
var _this = this; | ||
if (_a === void 0) { _a = {}; } | ||
var data = _a.data, otherOptions = __rest(_a, ["data"]); | ||
var _b = apiClient().get(this.url(), data, otherOptions), abort = _b.abort, promise = _b.promise; | ||
promise.then(function (data) { return _this.set(data, otherOptions); }); | ||
return this.withRequest('fetching', promise, abort); | ||
}; | ||
__decorate([ | ||
mobx.observable | ||
], Collection.prototype, "models", void 0); | ||
__decorate([ | ||
mobx.computed | ||
], Collection.prototype, "length", null); | ||
__decorate([ | ||
mobx.computed | ||
], Collection.prototype, "isEmpty", null); | ||
__decorate([ | ||
mobx.action | ||
], Collection.prototype, "add", null); | ||
__decorate([ | ||
mobx.action | ||
], Collection.prototype, "reset", null); | ||
__decorate([ | ||
mobx.action | ||
], Collection.prototype, "remove", null); | ||
__decorate([ | ||
mobx.action | ||
], Collection.prototype, "set", null); | ||
__decorate([ | ||
mobx.action | ||
], Collection.prototype, "create", null); | ||
__decorate([ | ||
mobx.action | ||
], Collection.prototype, "fetch", null); | ||
return Collection; | ||
}(Base)); | ||
exports.Collection = Collection; | ||
exports.ErrorObject = ErrorObject; | ||
exports.Model = Model; | ||
exports.Request = Request; | ||
exports.apiClient = apiClient; |
102
package.json
{ | ||
"name": "mobx-rest", | ||
"version": "2.2.5", | ||
"version": "3.0.0", | ||
"description": "REST conventions for mobx.", | ||
"jest": { | ||
"roots": [ | ||
"." | ||
], | ||
"transform": { | ||
".+\\.tsx?$": "ts-jest" | ||
}, | ||
"testRegex": "/__tests__/.*\\.spec\\.tsx?$" | ||
}, | ||
"repository": { | ||
@@ -10,73 +19,37 @@ "type": "git", | ||
"license": "MIT", | ||
"jest": { | ||
"collectCoverage": true, | ||
"testRegex": "/__tests__/.*\\.spec\\.js$", | ||
"collectCoverageFrom": [ | ||
"src/**/*.js" | ||
] | ||
}, | ||
"standard": { | ||
"parser": "babel-eslint", | ||
"globals": [ | ||
"it", | ||
"describe", | ||
"beforeEach", | ||
"expect", | ||
"Class", | ||
"jest" | ||
] | ||
}, | ||
"dependencies": { | ||
"lodash": "^4.17.4" | ||
}, | ||
"peerDependencies": { | ||
"mobx": "^3.2.0" | ||
"deepmerge": "3.2.0", | ||
"lodash": "4.17.11", | ||
"mobx": "5.9.4" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "^6.24.1", | ||
"babel-core": "^6.25.0", | ||
"babel-eslint": "^7.2.3", | ||
"babel-jest": "^20.0.3", | ||
"babel-plugin-lodash": "^3.3.2", | ||
"babel-plugin-transform-async-to-generator": "^6.24.1", | ||
"babel-plugin-transform-decorators-legacy": "^1.3.4", | ||
"babel-plugin-transform-flow-strip-types": "^6.22.0", | ||
"babel-plugin-transform-runtime": "^6.23.0", | ||
"babel-polyfill": "^6.23.0", | ||
"babel-preset-es2015": "^6.24.1", | ||
"babel-preset-stage-1": "^6.24.1", | ||
"babel-register": "^6.24.1", | ||
"eslint": "^3.19.0", | ||
"eslint-config-standard": "^10.2.1", | ||
"eslint-plugin-flowtype": "2.34.0", | ||
"eslint-plugin-import": "^2.3.0", | ||
"eslint-plugin-node": "^5.0.0", | ||
"eslint-plugin-promise": "^3.5.0", | ||
"eslint-plugin-standard": "^3.0.1", | ||
"flow-bin": "^0.47.0", | ||
"flow-copy-source": "^1.1.0", | ||
"husky": "^0.13.4", | ||
"jest": "^20.0.4", | ||
"lint-staged": "^3.6.0", | ||
"mobx": "^3.2.0", | ||
"prettier-standard": "^5.0.0", | ||
"rimraf": "^2.6.1" | ||
"@types/jest": "24.0.13", | ||
"@typescript-eslint/eslint-plugin": "1.9.0", | ||
"@typescript-eslint/parser": "1.9.0", | ||
"eslint": "5.16.0", | ||
"husky": "0.13.4", | ||
"jest": "24.8.0", | ||
"lint-staged": "3.6.0", | ||
"rimraf": "2.6.1", | ||
"rollup": "1.12.1", | ||
"rollup-plugin-node-resolve": "5.0.0", | ||
"rollup-plugin-typescript2": "^0.21.1", | ||
"ts-jest": "24.0.2", | ||
"tslib": "1.9.3", | ||
"typescript": "3.4.5" | ||
}, | ||
"main": "lib", | ||
"scripts": { | ||
"build": "yarn build:clean && rollup --config", | ||
"build:clean": "rimraf lib", | ||
"build:lib": "babel -d lib src --ignore '**/__tests__/**'", | ||
"build:flow": "flow-copy-source -v -i '**/__tests__/**' src lib", | ||
"build": "npm run build:clean && npm run build:lib && npm run build:flow", | ||
"prepublish": "npm run build", | ||
"jest": "BABEL_ENV=test NODE_PATH=src jest --no-cache", | ||
"lint": "eslint --ext .jsx,.js --cache src/ __tests__/", | ||
"flow": "flow", | ||
"test": "npm run flow && npm run lint && npm run jest", | ||
"format": "prettier-standard --print-width 60 \"{src,__tests__}/**/*.js\"", | ||
"prepush": "npm test", | ||
"build:lib": "yarn build:clean && rollup --config", | ||
"jest": "NODE_PATH=src jest --no-cache", | ||
"lint": "eslint --ext .ts --cache src/ __tests__/", | ||
"prepublish": "yarn build", | ||
"prepush": "yarn test", | ||
"test": "yarn lint && yarn jest", | ||
"watch": "rollup --config -w", | ||
"lint-staged": { | ||
"linters": { | ||
"{src|__tests__}/**/*.js": [ | ||
"prettier-standard", | ||
"git add" | ||
@@ -86,3 +59,8 @@ ] | ||
} | ||
}, | ||
"dependencies": { | ||
"deepmerge": "3.2.0", | ||
"lodash": "4.17.11", | ||
"mobx": "5.9.4" | ||
} | ||
} |
135
README.md
@@ -35,3 +35,3 @@ # mobx-rest | ||
- **Component state**: Each state can have their own state, like a button | ||
- **Component state**: Each component can have their own state, like a button | ||
being pressed, a text input value, etc. | ||
@@ -88,2 +88,6 @@ - **Application state**: Sometimes we need components to share state between them and | ||
#### `defaultAttributes: Object` | ||
An object literal that holds the default attributes of the model. {} by default. | ||
#### `attributes: ObservableMap` | ||
@@ -139,2 +143,67 @@ | ||
#### changedAttributes: Array<string> | ||
Get an array with the attributes names that have changed. | ||
Example: | ||
```js | ||
model.set({ name: 'Pau'}) | ||
model.changedAttributes // => ['name'] | ||
``` | ||
#### changes: { [string]: any } | ||
Gets the current changes. | ||
Example: | ||
```js | ||
model.set({ name: 'Pau'}) | ||
model.changes // => { name: 'Pau' } | ||
``` | ||
#### hasChanges(attribute?: string): boolean | ||
If an attribute is specified, returns true if it has changes. | ||
If no attribute is specified, returns true if any attribute has changes. | ||
Example: | ||
```js | ||
model.set({ name: 'Pau'}) | ||
// with attribute | ||
model.hasChanges('name') // => true | ||
// without attribute | ||
model.hasChanges() // => true | ||
``` | ||
#### commitChanges(): void | ||
Commit attributes to model. | ||
Example: | ||
```js | ||
model.set({ name: 'Pau' }) | ||
model.hasChanges // => true | ||
model.commitChanges() | ||
model.hasChanges // => false | ||
``` | ||
#### discardChanges(): void | ||
This will reset the model attributes to the last committed ones. | ||
Example: | ||
```js | ||
const model = new Model({ name: 'Foo' }) | ||
model.set({ name: 'Pau' }) | ||
model.get('name') // => Pau | ||
model.discardChanges() | ||
model.get('name') // => 'Foo' | ||
``` | ||
#### `isNew: boolean` | ||
@@ -240,3 +309,3 @@ | ||
#### `rpc(method: 'string', body: {}): Promise` | ||
#### `rpc(method: 'string', body?: {}, label?: 'fetching'): Promise` | ||
@@ -308,3 +377,3 @@ When dealing with REST there are always cases when we have some actions beyond | ||
#### `isEmpty(): boolean` | ||
#### `isEmpty: boolean` | ||
@@ -328,8 +397,12 @@ Helper method that asks the collection whether there is any | ||
#### `get(id: number): ?Model` | ||
#### `get(id: number, { required?: boolean = false }): ?Model` | ||
Find a model (or not) with the given id. | ||
Find a model (or not) with the given id. If `required` it will raise an error if not found. | ||
#### `filter(query: Object): Array<Model>` | ||
#### `mustGet(id: number): Model` | ||
Find a model with the given id or raise an Error. | ||
#### `filter(query: Object | Function): Array<Model>` | ||
Helper method that filters the collection by the given conditions represented | ||
@@ -345,6 +418,6 @@ as a key value. | ||
#### `find(query: Object): ?Model` | ||
#### `find(query: Object, { required?: boolean = false }): ?Model` | ||
Same as `filter` but it will halt and return when the first model matches | ||
the conditions. | ||
the conditions. If `required` it will raise an error if not found. | ||
@@ -356,6 +429,19 @@ Example: | ||
pau.get('name') // => 'pau' | ||
usersCollection.find({ name: 'foo'}) // => Error(`Invariant: Model must be found`) | ||
``` | ||
#### `add(data: Array<Object>): Array<Model>` | ||
#### `mustFind(query: Object): Model` | ||
Same as `find` but it will raise an Error if the model is not found. | ||
Example: | ||
```js | ||
const pau = usersCollection.mustFind({ name: 'pau' }) | ||
pau.get('name') // => 'pau' | ||
``` | ||
#### `add(data: Array<Object|T>|T|Object): Array<Model>` | ||
Adds models with the given array of attributes. | ||
@@ -367,3 +453,3 @@ | ||
#### `reset(data: Array<Object>): Array<Model>` | ||
#### `reset(data: Array<Object|T>): Array<Model>` | ||
@@ -376,3 +462,3 @@ Resets the collection with the given models. | ||
#### `remove(ids: Array<number>): void` | ||
#### `remove(ids: Array<number|T>|number|T): void` | ||
@@ -412,3 +498,3 @@ Remove any model with the given ids. | ||
#### `build(attributes: Object): Model` | ||
#### `build(attributes: Object|T): Model` | ||
@@ -458,9 +544,30 @@ Instantiates and links a model to the current collection. | ||
### forEach (callback: (model: T) => void): void | ||
Alias for models.forEach | ||
Example: `collection.forEach(model => console.log(model.get('id')))` | ||
### map<P> (callback: (model: T) => P): Array<P> | ||
Alias for models.map | ||
Example: `collection.map(model => model.get('id')) // => [1,2,3...]` | ||
### peek (): Array<T: Model> | ||
Returns a shallow array representation of the collection | ||
### slice (): Array<T: Model> | ||
Returns a defensive shallow array representation of the collection | ||
### `apiClient` | ||
This is the object that is going to make the `xhr` requests to interact with your API. | ||
There are currently two implementations: | ||
There are currently three implementations: | ||
- One using `jQuery` in the [mobx-rest-jquery-adapter](https://github.com/masylum/mobx-rest-jquery-adapter) package. | ||
- One using `fetch` in the [mobx-rest-fetch-adapter](https://github.com/masylum/mobx-rest-fetch-adapter) package. | ||
- One Using `axios` in the [mobx-rest-axios-adapter](https://github.com/IranTIP/mobx-rest-axios-adapter) package. | ||
@@ -540,3 +647,3 @@ Initially you need to configure `apiClient()` with an adapter and the `apiPath`. You can also set additional options, like headers to send with all requests. | ||
class Tasks extends React.Component { | ||
componentWillMount () { | ||
componentDidMount () { | ||
// This will call `/api/tasks?all=true` | ||
@@ -543,0 +650,0 @@ tasksCollection.fetch({ data: { all: true } }) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
14
733
1
61921
6
12
1
1118
+ Addeddeepmerge@3.2.0
+ Addedmobx@5.9.4
+ Addeddeepmerge@3.2.0(transitive)
+ Addedlodash@4.17.11(transitive)
+ Addedmobx@5.9.4(transitive)
- Removedlodash@4.17.21(transitive)
- Removedmobx@3.6.2(transitive)
Updatedlodash@4.17.11