From 5511f7e5ef78013148512136a1cc1693258a2ea8 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 25 Feb 2021 00:19:31 +0100 Subject: [PATCH 01/45] Add clone method for models with the option to also clone the changes --- src/Model.js | 59 ++++++++++++++++++++++++++++++++++++++++++ src/__tests__/Model.js | 29 +++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/Model.js b/src/Model.js index f2769e8..03233f1 100644 --- a/src/Model.js +++ b/src/Model.js @@ -412,6 +412,65 @@ export default class Model { return { data: [data], relations }; } + /** + * Makes this model a copy of the specified model + * It also clones the changes that were in the specified model. + * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. + * Cloning + * + * @param source {Model} - The model that should be copied + * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} + */ + copy(source, options = {copyChanges: true}){ + const copyChanges = options.copyChanges + + // Copy all fields and values from the specified model + this.parse(source.toJS()) + + + // Set only the changed attributes + if (copyChanges) { + this._copyChanges(source) + } else { + // Maintain the relations after copy + this.__activeRelations = source.__activeRelations; + this.__currentActiveRelations = source.__currentActiveRelations; + } + } + + /** + * Goes over model and all related models to set the changed values + * + * @param source + * @private + */ + _copyChanges(source) { + // Maintain the relations after copy + this.__activeRelations = source.__activeRelations; + this.__currentActiveRelations = source.__currentActiveRelations; + + // Copy all changed fields and notify the store that there are changes + if (source.__changes.length > 0) { + this.__store.__setChanged = true; + source.__changes.forEach((changedAttribute) => { + this.setInput(changedAttribute, source[changedAttribute]) + }) + } + + + // Set the changes for all related models with changes + source.__activeRelations.forEach((relation) => { + if (relation && source[relation]) { + if (source[relation].hasUserChanges) { + // Set the changes for all related models with changes + source[relation].models.forEach((relatedModel,index) => { + this[relation].models[index]._copyChanges(relatedModel); + }); + } + } + }); + } + toJS() { const output = {}; this.__attributes.forEach(attr => { diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index ecda291..25c504f 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1825,3 +1825,32 @@ describe('changes', () => { expect(animal.hasUserChanges).toBe(true); }); }); + + +test('clone', () => { + const customer = new Customer(null, { + relations: ['oldTowns.bestCook.workPlaces'], + }); + + customer.fromBackend({ + data: customersWithTownCookRestaurant.data, + repos: customersWithTownCookRestaurant.with, + relMapping: customersWithTownCookRestaurant.with_mapping, + }); + + customer.oldTowns.at(0).bestCook.at(0).workPlaces.at(0).setInput('name', "Italian"); + + const customerCopyWithChanges = new Customer(); + customerCopyWithChanges.copy(customer) + + // Clone with changes should give the same toBackend result as the cloned object + expect(customerCopyWithChanges.toBackendAll({onlyChanges: true})).toBe(customer.toBackendAll({onlyChanges: true})) + + const customerCopyNoChanges = new Customer(); + customerCopyNoChanges.copy(customer, {copyChanges: false}) + + // Clone without changes should give the same toBackend result as the cloned object when only changes is false + expect(customerCopyWithChanges.toBackendAll({onlyChanges: false})).toBe(customer.toBackendAll({onlyChanges: false})) +}); + + From fd1a2cca4fe2b71381f9760f3901d09db8c1c418 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 25 Feb 2021 10:40:22 +0100 Subject: [PATCH 02/45] Fix testing and parsing relations from copied model --- src/Model.js | 13 +++++++------ src/__tests__/Model.js | 28 ++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Model.js b/src/Model.js index 03233f1..6203a80 100644 --- a/src/Model.js +++ b/src/Model.js @@ -207,7 +207,7 @@ export default class Model { activeRelations.forEach(aRel => { // If aRel is null, this relation is already defined by another aRel // IE.: town.restaurants.chef && town - if (aRel === null) { + if (aRel === null || !!this[aRel]) { return; } const relNames = aRel.match(RE_SPLIT_FIRST_RELATION); @@ -232,7 +232,7 @@ export default class Model { }); extendObservable( this, - mapValues(relModels, (otherRelNames, relName) => { + mapValues(omit(relModels, Object.keys(relModels).filter(rel => !!this[rel])), (otherRelNames, relName) => { const RelModel = relations[relName]; invariant( RelModel, @@ -424,6 +424,11 @@ export default class Model { copy(source, options = {copyChanges: true}){ const copyChanges = options.copyChanges + // Maintain the relations after copy + // this.__activeRelations = source.__activeRelations; + this.__currentActiveRelations = source.__currentActiveRelations; + + this.__parseRelations(source.__activeRelations) // Copy all fields and values from the specified model this.parse(source.toJS()) @@ -431,10 +436,6 @@ export default class Model { // Set only the changed attributes if (copyChanges) { this._copyChanges(source) - } else { - // Maintain the relations after copy - this.__activeRelations = source.__activeRelations; - this.__currentActiveRelations = source.__currentActiveRelations; } } diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index 25c504f..eb6a01d 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1827,7 +1827,7 @@ describe('changes', () => { }); -test('clone', () => { +test('copy (with changes)', () => { const customer = new Customer(null, { relations: ['oldTowns.bestCook.workPlaces'], }); @@ -1838,19 +1838,35 @@ test('clone', () => { relMapping: customersWithTownCookRestaurant.with_mapping, }); - customer.oldTowns.at(0).bestCook.at(0).workPlaces.at(0).setInput('name', "Italian"); - const customerCopyWithChanges = new Customer(); + customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); + + const customerCopyWithChanges = new Customer(null, {relations: ['oldTowns.bestCook']}); customerCopyWithChanges.copy(customer) // Clone with changes should give the same toBackend result as the cloned object - expect(customerCopyWithChanges.toBackendAll({onlyChanges: true})).toBe(customer.toBackendAll({onlyChanges: true})) + expect(customerCopyWithChanges.toBackendAll({ onlyChanges: true })).toEqual(customer.toBackendAll({ onlyChanges: true })) +}); + +test('copy (without changes)', () => { + const customer = new Customer(null, { + relations: ['oldTowns.bestCook.workPlaces'], + }); + + customer.fromBackend({ + data: customersWithTownCookRestaurant.data, + repos: customersWithTownCookRestaurant.with, + relMapping: customersWithTownCookRestaurant.with_mapping, + }); + + customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); const customerCopyNoChanges = new Customer(); - customerCopyNoChanges.copy(customer, {copyChanges: false}) + customerCopyNoChanges.copy(customer, {copyChanges: true}) + // Clone without changes should give the same toBackend result as the cloned object when only changes is false - expect(customerCopyWithChanges.toBackendAll({onlyChanges: false})).toBe(customer.toBackendAll({onlyChanges: false})) + expect(customerCopyNoChanges.toBackendAll({onlyChanges: false})).toEqual(customer.toBackendAll({onlyChanges: false})) }); From 598d11d5f8867cf74c0f2813d27ee3963fcce856 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 25 Feb 2021 10:59:06 +0100 Subject: [PATCH 03/45] add comments --- src/Model.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Model.js b/src/Model.js index 6203a80..af266a1 100644 --- a/src/Model.js +++ b/src/Model.js @@ -230,6 +230,7 @@ export default class Model { this.__activeCurrentRelations.push(currentRel); } }); + // extendObservable where we omit the fields that are already created from other relations extendObservable( this, mapValues(omit(relModels, Object.keys(relModels).filter(rel => !!this[rel])), (otherRelNames, relName) => { From fe3bb5a501f4aa0533e105cf107a30e2101b8656 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 25 Feb 2021 11:00:49 +0100 Subject: [PATCH 04/45] Create new build --- dist/mobx-spine.cjs.js | 193 ++++++++++++++++++++++++++++------------- dist/mobx-spine.es.js | 193 ++++++++++++++++++++++++++++------------- 2 files changed, 264 insertions(+), 122 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index ba8a91d..d02b883 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -986,7 +986,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { activeRelations.forEach(function (aRel) { // If aRel is null, this relation is already defined by another aRel // IE.: town.restaurants.chef && town - if (aRel === null) { + if (aRel === null || !!_this3[aRel]) { return; } var relNames = aRel.match(RE_SPLIT_FIRST_RELATION); @@ -1004,7 +1004,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { _this3.__activeCurrentRelations.push(currentRel); } }); - mobx.extendObservable(this, lodash.mapValues(relModels, function (otherRelNames, relName) { + // extendObservable where we omit the fields that are already created from other relations + mobx.extendObservable(this, lodash.mapValues(lodash.omit(relModels, Object.keys(relModels).filter(function (rel) { + return !!_this3[rel]; + })), function (otherRelNames, relName) { var RelModel = relations[relName]; invariant(RelModel, 'Specified relation "' + relName + '" does not exist on model.'); var options = { relations: otherRelNames }; @@ -1158,18 +1161,86 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return { data: [data], relations: relations }; } + + /** + * Makes this model a copy of the specified model + * It also clones the changes that were in the specified model. + * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. + * Cloning + * + * @param source {Model} - The model that should be copied + * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} + */ + + }, { + key: 'copy', + value: function copy(source) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; + + var copyChanges = options.copyChanges; + + // Maintain the relations after copy + // this.__activeRelations = source.__activeRelations; + this.__currentActiveRelations = source.__currentActiveRelations; + + this.__parseRelations(source.__activeRelations); + // Copy all fields and values from the specified model + this.parse(source.toJS()); + + // Set only the changed attributes + if (copyChanges) { + this._copyChanges(source); + } + } + + /** + * Goes over model and all related models to set the changed values + * + * @param source + * @private + */ + + }, { + key: '_copyChanges', + value: function _copyChanges(source) { + var _this6 = this; + + // Maintain the relations after copy + this.__activeRelations = source.__activeRelations; + this.__currentActiveRelations = source.__currentActiveRelations; + + // Copy all changed fields and notify the store that there are changes + if (source.__changes.length > 0) { + this.__store.__setChanged = true; + source.__changes.forEach(function (changedAttribute) { + _this6.setInput(changedAttribute, source[changedAttribute]); + }); + } + + // Set the changes for all related models with changes + source.__activeRelations.forEach(function (relation) { + if (relation && source[relation]) { + if (source[relation].hasUserChanges) { + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index]._copyChanges(relatedModel); + }); + } + } + }); + } }, { key: 'toJS', value: function toJS() { - var _this6 = this; + var _this7 = this; var output = {}; this.__attributes.forEach(function (attr) { - output[attr] = _this6.__toJSAttr(attr, _this6[attr]); + output[attr] = _this7.__toJSAttr(attr, _this7[attr]); }); this.__activeCurrentRelations.forEach(function (currentRel) { - var model = _this6[currentRel]; + var model = _this7[currentRel]; if (model) { output[currentRel] = model.toJS(); } @@ -1224,7 +1295,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__scopeBackendResponse', value: function __scopeBackendResponse(_ref2) { - var _this7 = this; + var _this8 = this; var data = _ref2.data, targetRelName = _ref2.targetRelName, @@ -1246,18 +1317,18 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var repository = repos[repoName]; // For backwards compatibility, reverseMapping is optional (for now) var reverseRelName = reverseMapping ? reverseMapping[backendRelName] : null; - var relName = _this7.constructor.fromBackendAttrKey(backendRelName); + var relName = _this8.constructor.fromBackendAttrKey(backendRelName); if (targetRelName === relName) { - var relKey = data[_this7.constructor.toBackendAttrKey(relName)]; + var relKey = data[_this8.constructor.toBackendAttrKey(relName)]; if (relKey !== undefined) { relevant = true; - scopedData = _this7.__parseRepositoryToData(relKey, repository); + scopedData = _this8.__parseRepositoryToData(relKey, repository); } else if (repository && reverseRelName) { - var pk = data[_this7.constructor.primaryKey]; + var pk = data[_this8.constructor.primaryKey]; relevant = true; - scopedData = _this7.__parseReverseRepositoryToData(reverseRelName, pk, repository); - if (_this7.relations(relName).prototype instanceof Model) { + scopedData = _this8.__parseReverseRepositoryToData(reverseRelName, pk, repository); + if (_this8.relations(relName).prototype instanceof Model) { if (scopedData.length === 0) { scopedData = null; } else if (scopedData.length === 1) { @@ -1297,7 +1368,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fromBackend', value: function fromBackend(_ref3) { - var _this8 = this; + var _this9 = this; var data = _ref3.data, repos = _ref3.repos, @@ -1310,8 +1381,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // So when we have a model with a `town.restaurants.chef` relation, // we call fromBackend on the `town` relation. lodash.each(this.__activeCurrentRelations, function (relName) { - var rel = _this8[relName]; - var resScoped = _this8.__scopeBackendResponse({ + var rel = _this9[relName]; + var resScoped = _this9.__scopeBackendResponse({ data: data, targetRelName: relName, repos: repos, @@ -1353,22 +1424,22 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parse', value: function parse(data) { - var _this9 = this; + var _this10 = this; invariant(lodash.isPlainObject(data), 'Parameter supplied to `parse()` is not an object, got: ' + JSON.stringify(data)); lodash.forIn(data, function (value, key) { - var attr = _this9.constructor.fromBackendAttrKey(key); - if (_this9.__attributes.includes(attr)) { - _this9[attr] = _this9.__parseAttr(attr, value); - } else if (_this9.__activeCurrentRelations.includes(attr)) { + var attr = _this10.constructor.fromBackendAttrKey(key); + if (_this10.__attributes.includes(attr)) { + _this10[attr] = _this10.__parseAttr(attr, value); + } else if (_this10.__activeCurrentRelations.includes(attr)) { // In Binder, a relation property is an `int` or `[int]`, referring to its ID. // However, it can also be an object if there are nested relations (non flattened). if (lodash.isPlainObject(value) || lodash.isPlainObject(lodash.get(value, '[0]'))) { - _this9[attr].parse(value); + _this10[attr].parse(value); } else if (value === null) { // The relation is cleared. - _this9[attr].clear(); + _this10[attr].clear(); } } }); @@ -1388,7 +1459,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'saveFile', value: function saveFile(name) { - var _this10 = this; + var _this11 = this; var snakeName = camelToSnake(name); @@ -1399,16 +1470,16 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data.append(name, file, file.name); return this.api.post('' + this.url + snakeName + '/', data, { headers: { 'Content-Type': 'multipart/form-data' } }).then(mobx.action(function (res) { - _this10.__fileExists[name] = true; - delete _this10.__fileChanges[name]; - _this10.saveFromBackend(res); + _this11.__fileExists[name] = true; + delete _this11.__fileChanges[name]; + _this11.saveFromBackend(res); })); } else if (this.__fileDeletions[name]) { if (this.__fileExists[name]) { return this.api.delete('' + this.url + snakeName + '/').then(mobx.action(function () { - _this10.__fileExists[name] = false; - delete _this10.__fileDeletions[name]; - _this10.saveFromBackend({ data: defineProperty({}, snakeName, null) }); + _this11.__fileExists[name] = false; + delete _this11.__fileDeletions[name]; + _this11.saveFromBackend({ data: defineProperty({}, snakeName, null) }); })); } else { delete this.__fileDeletions[name]; @@ -1425,7 +1496,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'save', value: function save() { - var _this11 = this; + var _this12 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1441,17 +1512,17 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { isNew: this.isNew, requestOptions: lodash.omit(options, 'url', 'data', 'mapData') }).then(mobx.action(function (res) { - _this11.saveFromBackend(_extends({}, res, { - data: lodash.omit(res.data, _this11.fileFields().map(camelToSnake)) + _this12.saveFromBackend(_extends({}, res, { + data: lodash.omit(res.data, _this12.fileFields().map(camelToSnake)) })); - _this11.clearUserFieldChanges(); - return _this11.saveFiles().then(function () { - _this11.clearUserFileChanges(); + _this12.clearUserFieldChanges(); + return _this12.saveFiles().then(function () { + _this12.clearUserFileChanges(); return Promise.resolve(res); }); })).catch(mobx.action(function (err) { if (err.valErrors) { - _this11.parseValidationErrors(err.valErrors); + _this12.parseValidationErrors(err.valErrors); } throw err; }))); @@ -1535,7 +1606,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'saveAll', value: function saveAll() { - var _this12 = this; + var _this13 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1551,10 +1622,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }), requestOptions: lodash.omit(options, 'relations', 'data', 'mapData') }).then(mobx.action(function (res) { - _this12.saveFromBackend(res); - _this12.clearUserFieldChanges(); + _this13.saveFromBackend(res); + _this13.clearUserFieldChanges(); - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { if (relation instanceof Model) { relation.clearUserFieldChanges(); } else { @@ -1562,10 +1633,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } }); - return _this12.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this12.clearUserFileChanges(); + return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + _this13.clearUserFileChanges(); - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { if (relation instanceof Model) { relation.clearUserFileChanges(); } @@ -1575,7 +1646,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }); })).catch(mobx.action(function (err) { if (err.valErrors) { - _this12.parseValidationErrors(err.valErrors); + _this13.parseValidationErrors(err.valErrors); } throw err; }))); @@ -1587,19 +1658,19 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__parseNewIds', value: function __parseNewIds(idMaps) { - var _this13 = this; + var _this14 = this; var bName = this.constructor.backendResourceName; if (bName && idMaps[bName]) { var idMap = idMaps[bName].find(function (ids) { - return ids[0] === _this13.getInternalId(); + return ids[0] === _this14.getInternalId(); }); if (idMap) { this[this.constructor.primaryKey] = idMap[1]; } } lodash.each(this.__activeCurrentRelations, function (relName) { - var rel = _this13[relName]; + var rel = _this14[relName]; rel.__parseNewIds(idMaps); }); } @@ -1611,7 +1682,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parseValidationErrors', value: function parseValidationErrors(valErrors) { - var _this14 = this; + var _this15 = this; var bname = this.constructor.backendResourceName; @@ -1624,24 +1695,24 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return snakeToCamel(key); }); var formattedErrors = lodash.mapValues(camelCasedErrors, function (valError) { - return valError.map(_this14.validationErrorFormatter); + return valError.map(_this15.validationErrorFormatter); }); this.__backendValidationErrors = formattedErrors; } } this.__activeCurrentRelations.forEach(function (currentRel) { - _this14[currentRel].parseValidationErrors(valErrors); + _this15[currentRel].parseValidationErrors(valErrors); }); } }, { key: 'clearValidationErrors', value: function clearValidationErrors() { - var _this15 = this; + var _this16 = this; this.__backendValidationErrors = {}; this.__activeCurrentRelations.forEach(function (currentRel) { - _this15[currentRel].clearValidationErrors(); + _this16[currentRel].clearValidationErrors(); }); } @@ -1659,12 +1730,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'delete', value: function _delete() { - var _this16 = this; + var _this17 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var removeFromStore = function removeFromStore() { - return _this16.__store ? _this16.__store.remove(_this16) : null; + return _this17.__store ? _this17.__store.remove(_this17) : null; }; if (options.immediate || this.isNew) { removeFromStore(); @@ -1690,7 +1761,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fetch', value: function fetch() { - var _this17 = this; + var _this18 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1702,7 +1773,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data: data, requestOptions: lodash.omit(options, ['data', 'url']) }).then(mobx.action(function (res) { - _this17.fromBackend(res); + _this18.fromBackend(res); }))); return promise; @@ -1710,26 +1781,26 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'clear', value: function clear() { - var _this18 = this; + var _this19 = this; lodash.forIn(this.__originalAttributes, function (value, key) { - _this18[key] = value; + _this19[key] = value; }); this.__activeCurrentRelations.forEach(function (currentRel) { - _this18[currentRel].clear(); + _this19[currentRel].clear(); }); } }, { key: 'hasUserChanges', get: function get$$1() { - var _this19 = this; + var _this20 = this; if (this.__changes.length > 0) { return true; } return this.__activeCurrentRelations.some(function (rel) { - return _this19[rel].hasUserChanges; + return _this20[rel].hasUserChanges; }); } }, { diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index ab52885..13d299b 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -980,7 +980,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { activeRelations.forEach(function (aRel) { // If aRel is null, this relation is already defined by another aRel // IE.: town.restaurants.chef && town - if (aRel === null) { + if (aRel === null || !!_this3[aRel]) { return; } var relNames = aRel.match(RE_SPLIT_FIRST_RELATION); @@ -998,7 +998,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { _this3.__activeCurrentRelations.push(currentRel); } }); - extendObservable(this, mapValues(relModels, function (otherRelNames, relName) { + // extendObservable where we omit the fields that are already created from other relations + extendObservable(this, mapValues(omit(relModels, Object.keys(relModels).filter(function (rel) { + return !!_this3[rel]; + })), function (otherRelNames, relName) { var RelModel = relations[relName]; invariant(RelModel, 'Specified relation "' + relName + '" does not exist on model.'); var options = { relations: otherRelNames }; @@ -1152,18 +1155,86 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return { data: [data], relations: relations }; } + + /** + * Makes this model a copy of the specified model + * It also clones the changes that were in the specified model. + * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. + * Cloning + * + * @param source {Model} - The model that should be copied + * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} + */ + + }, { + key: 'copy', + value: function copy(source) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; + + var copyChanges = options.copyChanges; + + // Maintain the relations after copy + // this.__activeRelations = source.__activeRelations; + this.__currentActiveRelations = source.__currentActiveRelations; + + this.__parseRelations(source.__activeRelations); + // Copy all fields and values from the specified model + this.parse(source.toJS()); + + // Set only the changed attributes + if (copyChanges) { + this._copyChanges(source); + } + } + + /** + * Goes over model and all related models to set the changed values + * + * @param source + * @private + */ + + }, { + key: '_copyChanges', + value: function _copyChanges(source) { + var _this6 = this; + + // Maintain the relations after copy + this.__activeRelations = source.__activeRelations; + this.__currentActiveRelations = source.__currentActiveRelations; + + // Copy all changed fields and notify the store that there are changes + if (source.__changes.length > 0) { + this.__store.__setChanged = true; + source.__changes.forEach(function (changedAttribute) { + _this6.setInput(changedAttribute, source[changedAttribute]); + }); + } + + // Set the changes for all related models with changes + source.__activeRelations.forEach(function (relation) { + if (relation && source[relation]) { + if (source[relation].hasUserChanges) { + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index]._copyChanges(relatedModel); + }); + } + } + }); + } }, { key: 'toJS', value: function toJS$$1() { - var _this6 = this; + var _this7 = this; var output = {}; this.__attributes.forEach(function (attr) { - output[attr] = _this6.__toJSAttr(attr, _this6[attr]); + output[attr] = _this7.__toJSAttr(attr, _this7[attr]); }); this.__activeCurrentRelations.forEach(function (currentRel) { - var model = _this6[currentRel]; + var model = _this7[currentRel]; if (model) { output[currentRel] = model.toJS(); } @@ -1218,7 +1289,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__scopeBackendResponse', value: function __scopeBackendResponse(_ref2) { - var _this7 = this; + var _this8 = this; var data = _ref2.data, targetRelName = _ref2.targetRelName, @@ -1240,18 +1311,18 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var repository = repos[repoName]; // For backwards compatibility, reverseMapping is optional (for now) var reverseRelName = reverseMapping ? reverseMapping[backendRelName] : null; - var relName = _this7.constructor.fromBackendAttrKey(backendRelName); + var relName = _this8.constructor.fromBackendAttrKey(backendRelName); if (targetRelName === relName) { - var relKey = data[_this7.constructor.toBackendAttrKey(relName)]; + var relKey = data[_this8.constructor.toBackendAttrKey(relName)]; if (relKey !== undefined) { relevant = true; - scopedData = _this7.__parseRepositoryToData(relKey, repository); + scopedData = _this8.__parseRepositoryToData(relKey, repository); } else if (repository && reverseRelName) { - var pk = data[_this7.constructor.primaryKey]; + var pk = data[_this8.constructor.primaryKey]; relevant = true; - scopedData = _this7.__parseReverseRepositoryToData(reverseRelName, pk, repository); - if (_this7.relations(relName).prototype instanceof Model) { + scopedData = _this8.__parseReverseRepositoryToData(reverseRelName, pk, repository); + if (_this8.relations(relName).prototype instanceof Model) { if (scopedData.length === 0) { scopedData = null; } else if (scopedData.length === 1) { @@ -1291,7 +1362,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fromBackend', value: function fromBackend(_ref3) { - var _this8 = this; + var _this9 = this; var data = _ref3.data, repos = _ref3.repos, @@ -1304,8 +1375,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // So when we have a model with a `town.restaurants.chef` relation, // we call fromBackend on the `town` relation. each(this.__activeCurrentRelations, function (relName) { - var rel = _this8[relName]; - var resScoped = _this8.__scopeBackendResponse({ + var rel = _this9[relName]; + var resScoped = _this9.__scopeBackendResponse({ data: data, targetRelName: relName, repos: repos, @@ -1347,22 +1418,22 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parse', value: function parse(data) { - var _this9 = this; + var _this10 = this; invariant(isPlainObject(data), 'Parameter supplied to `parse()` is not an object, got: ' + JSON.stringify(data)); forIn(data, function (value, key) { - var attr = _this9.constructor.fromBackendAttrKey(key); - if (_this9.__attributes.includes(attr)) { - _this9[attr] = _this9.__parseAttr(attr, value); - } else if (_this9.__activeCurrentRelations.includes(attr)) { + var attr = _this10.constructor.fromBackendAttrKey(key); + if (_this10.__attributes.includes(attr)) { + _this10[attr] = _this10.__parseAttr(attr, value); + } else if (_this10.__activeCurrentRelations.includes(attr)) { // In Binder, a relation property is an `int` or `[int]`, referring to its ID. // However, it can also be an object if there are nested relations (non flattened). if (isPlainObject(value) || isPlainObject(get(value, '[0]'))) { - _this9[attr].parse(value); + _this10[attr].parse(value); } else if (value === null) { // The relation is cleared. - _this9[attr].clear(); + _this10[attr].clear(); } } }); @@ -1382,7 +1453,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'saveFile', value: function saveFile(name) { - var _this10 = this; + var _this11 = this; var snakeName = camelToSnake(name); @@ -1393,16 +1464,16 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data.append(name, file, file.name); return this.api.post('' + this.url + snakeName + '/', data, { headers: { 'Content-Type': 'multipart/form-data' } }).then(action(function (res) { - _this10.__fileExists[name] = true; - delete _this10.__fileChanges[name]; - _this10.saveFromBackend(res); + _this11.__fileExists[name] = true; + delete _this11.__fileChanges[name]; + _this11.saveFromBackend(res); })); } else if (this.__fileDeletions[name]) { if (this.__fileExists[name]) { return this.api.delete('' + this.url + snakeName + '/').then(action(function () { - _this10.__fileExists[name] = false; - delete _this10.__fileDeletions[name]; - _this10.saveFromBackend({ data: defineProperty({}, snakeName, null) }); + _this11.__fileExists[name] = false; + delete _this11.__fileDeletions[name]; + _this11.saveFromBackend({ data: defineProperty({}, snakeName, null) }); })); } else { delete this.__fileDeletions[name]; @@ -1419,7 +1490,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'save', value: function save() { - var _this11 = this; + var _this12 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1435,17 +1506,17 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { isNew: this.isNew, requestOptions: omit(options, 'url', 'data', 'mapData') }).then(action(function (res) { - _this11.saveFromBackend(_extends({}, res, { - data: omit(res.data, _this11.fileFields().map(camelToSnake)) + _this12.saveFromBackend(_extends({}, res, { + data: omit(res.data, _this12.fileFields().map(camelToSnake)) })); - _this11.clearUserFieldChanges(); - return _this11.saveFiles().then(function () { - _this11.clearUserFileChanges(); + _this12.clearUserFieldChanges(); + return _this12.saveFiles().then(function () { + _this12.clearUserFileChanges(); return Promise.resolve(res); }); })).catch(action(function (err) { if (err.valErrors) { - _this11.parseValidationErrors(err.valErrors); + _this12.parseValidationErrors(err.valErrors); } throw err; }))); @@ -1529,7 +1600,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'saveAll', value: function saveAll() { - var _this12 = this; + var _this13 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1545,10 +1616,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }), requestOptions: omit(options, 'relations', 'data', 'mapData') }).then(action(function (res) { - _this12.saveFromBackend(res); - _this12.clearUserFieldChanges(); + _this13.saveFromBackend(res); + _this13.clearUserFieldChanges(); - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { if (relation instanceof Model) { relation.clearUserFieldChanges(); } else { @@ -1556,10 +1627,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } }); - return _this12.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this12.clearUserFileChanges(); + return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + _this13.clearUserFileChanges(); - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { if (relation instanceof Model) { relation.clearUserFileChanges(); } @@ -1569,7 +1640,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }); })).catch(action(function (err) { if (err.valErrors) { - _this12.parseValidationErrors(err.valErrors); + _this13.parseValidationErrors(err.valErrors); } throw err; }))); @@ -1581,19 +1652,19 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__parseNewIds', value: function __parseNewIds(idMaps) { - var _this13 = this; + var _this14 = this; var bName = this.constructor.backendResourceName; if (bName && idMaps[bName]) { var idMap = idMaps[bName].find(function (ids) { - return ids[0] === _this13.getInternalId(); + return ids[0] === _this14.getInternalId(); }); if (idMap) { this[this.constructor.primaryKey] = idMap[1]; } } each(this.__activeCurrentRelations, function (relName) { - var rel = _this13[relName]; + var rel = _this14[relName]; rel.__parseNewIds(idMaps); }); } @@ -1605,7 +1676,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parseValidationErrors', value: function parseValidationErrors(valErrors) { - var _this14 = this; + var _this15 = this; var bname = this.constructor.backendResourceName; @@ -1618,24 +1689,24 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return snakeToCamel(key); }); var formattedErrors = mapValues(camelCasedErrors, function (valError) { - return valError.map(_this14.validationErrorFormatter); + return valError.map(_this15.validationErrorFormatter); }); this.__backendValidationErrors = formattedErrors; } } this.__activeCurrentRelations.forEach(function (currentRel) { - _this14[currentRel].parseValidationErrors(valErrors); + _this15[currentRel].parseValidationErrors(valErrors); }); } }, { key: 'clearValidationErrors', value: function clearValidationErrors() { - var _this15 = this; + var _this16 = this; this.__backendValidationErrors = {}; this.__activeCurrentRelations.forEach(function (currentRel) { - _this15[currentRel].clearValidationErrors(); + _this16[currentRel].clearValidationErrors(); }); } @@ -1653,12 +1724,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'delete', value: function _delete() { - var _this16 = this; + var _this17 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var removeFromStore = function removeFromStore() { - return _this16.__store ? _this16.__store.remove(_this16) : null; + return _this17.__store ? _this17.__store.remove(_this17) : null; }; if (options.immediate || this.isNew) { removeFromStore(); @@ -1684,7 +1755,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fetch', value: function fetch() { - var _this17 = this; + var _this18 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1696,7 +1767,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data: data, requestOptions: omit(options, ['data', 'url']) }).then(action(function (res) { - _this17.fromBackend(res); + _this18.fromBackend(res); }))); return promise; @@ -1704,26 +1775,26 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'clear', value: function clear() { - var _this18 = this; + var _this19 = this; forIn(this.__originalAttributes, function (value, key) { - _this18[key] = value; + _this19[key] = value; }); this.__activeCurrentRelations.forEach(function (currentRel) { - _this18[currentRel].clear(); + _this19[currentRel].clear(); }); } }, { key: 'hasUserChanges', get: function get$$1() { - var _this19 = this; + var _this20 = this; if (this.__changes.length > 0) { return true; } return this.__activeCurrentRelations.some(function (rel) { - return _this19[rel].hasUserChanges; + return _this20[rel].hasUserChanges; }); } }, { From b08796487e93b8af79956ec83b5fec41b0af6537 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 25 Feb 2021 12:41:49 +0100 Subject: [PATCH 05/45] Fixed small bug --- dist/mobx-spine.cjs.js | 4 +++- dist/mobx-spine.es.js | 4 +++- src/Model.js | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index d02b883..241eb7f 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1211,7 +1211,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { - this.__store.__setChanged = true; + if (this.__store) { + this.__store.__setChanged = true; + } source.__changes.forEach(function (changedAttribute) { _this6.setInput(changedAttribute, source[changedAttribute]); }); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 13d299b..785ad18 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1205,7 +1205,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { - this.__store.__setChanged = true; + if (this.__store) { + this.__store.__setChanged = true; + } source.__changes.forEach(function (changedAttribute) { _this6.setInput(changedAttribute, source[changedAttribute]); }); diff --git a/src/Model.js b/src/Model.js index af266a1..0114879 100644 --- a/src/Model.js +++ b/src/Model.js @@ -453,7 +453,9 @@ export default class Model { // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { - this.__store.__setChanged = true; + if (this.__store) { + this.__store.__setChanged = true; + } source.__changes.forEach((changedAttribute) => { this.setInput(changedAttribute, source[changedAttribute]) }) From 01e511dd3d5b538d656cfc9a9774c65f13506ec1 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 25 Feb 2021 12:54:06 +0100 Subject: [PATCH 06/45] Add different way to notify store of changes --- src/Model.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Model.js b/src/Model.js index 0114879..4435d3c 100644 --- a/src/Model.js +++ b/src/Model.js @@ -441,21 +441,25 @@ export default class Model { } /** - * Goes over model and all related models to set the changed values + * Goes over model and all related models to set the changed values and notify the store * - * @param source + * @param source - the model to copy + * @param store - the store of the current model, to setChanged if there are changes * @private */ - _copyChanges(source) { + _copyChanges(source, store) { // Maintain the relations after copy this.__activeRelations = source.__activeRelations; this.__currentActiveRelations = source.__currentActiveRelations; // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { - if (this.__store) { + if (store) { + store.__setChanged = true; + } else if (this.__store) { this.__store.__setChanged = true; } + source.__changes.forEach((changedAttribute) => { this.setInput(changedAttribute, source[changedAttribute]) }) @@ -468,7 +472,7 @@ export default class Model { if (source[relation].hasUserChanges) { // Set the changes for all related models with changes source[relation].models.forEach((relatedModel,index) => { - this[relation].models[index]._copyChanges(relatedModel); + this[relation].models[index]._copyChanges(relatedModel, this[relation]); }); } } From bbf1556e906a8c6bc8fdd7621162236e5ed7b427 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 25 Feb 2021 12:54:29 +0100 Subject: [PATCH 07/45] add build --- dist/mobx-spine.cjs.js | 14 +++++++++----- dist/mobx-spine.es.js | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 241eb7f..a29aad3 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1194,15 +1194,16 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } /** - * Goes over model and all related models to set the changed values + * Goes over model and all related models to set the changed values and notify the store * - * @param source + * @param source - the model to copy + * @param store - the store of the current model, to setChanged if there are changes * @private */ }, { key: '_copyChanges', - value: function _copyChanges(source) { + value: function _copyChanges(source, store) { var _this6 = this; // Maintain the relations after copy @@ -1211,9 +1212,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { - if (this.__store) { + if (store) { + store.__setChanged = true; + } else if (this.__store) { this.__store.__setChanged = true; } + source.__changes.forEach(function (changedAttribute) { _this6.setInput(changedAttribute, source[changedAttribute]); }); @@ -1225,7 +1229,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (source[relation].hasUserChanges) { // Set the changes for all related models with changes source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index]._copyChanges(relatedModel); + _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); }); } } diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 785ad18..20df20b 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1188,15 +1188,16 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } /** - * Goes over model and all related models to set the changed values + * Goes over model and all related models to set the changed values and notify the store * - * @param source + * @param source - the model to copy + * @param store - the store of the current model, to setChanged if there are changes * @private */ }, { key: '_copyChanges', - value: function _copyChanges(source) { + value: function _copyChanges(source, store) { var _this6 = this; // Maintain the relations after copy @@ -1205,9 +1206,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { - if (this.__store) { + if (store) { + store.__setChanged = true; + } else if (this.__store) { this.__store.__setChanged = true; } + source.__changes.forEach(function (changedAttribute) { _this6.setInput(changedAttribute, source[changedAttribute]); }); @@ -1219,7 +1223,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (source[relation].hasUserChanges) { // Set the changes for all related models with changes source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index]._copyChanges(relatedModel); + _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); }); } } From c259fa6ca6548d00b2758508a84a273a4d4f4dea Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 9 Mar 2021 16:41:03 +0100 Subject: [PATCH 08/45] Copy a model returns a copy of the model when no model to copy is given --- src/Model.js | 32 ++++++++++++++++++++++++++------ src/__tests__/Model.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/Model.js b/src/Model.js index 4435d3c..4ee9c21 100644 --- a/src/Model.js +++ b/src/Model.js @@ -415,6 +415,7 @@ export default class Model { /** * Makes this model a copy of the specified model + * or returns a copy of the current model when no model to copy is given * It also clones the changes that were in the specified model. * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. * Cloning @@ -422,22 +423,41 @@ export default class Model { * @param source {Model} - The model that should be copied * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} */ - copy(source, options = {copyChanges: true}){ - const copyChanges = options.copyChanges + copy(source= undefined, options = {copyChanges: true}){ + let copiedModel; + // If our source is not a model it is 'probably' the options + if (source !== undefined && !(source instanceof Model)){ + options = source; + source = undefined; + } + + // Make sure that we have the correct model + if (source === undefined){ + source = this; + copiedModel = new source.constructor(); + } else if (this.constructor !== source.constructor) { + copiedModel = new source.constructor(); + } else { + copiedModel = this; + } + + const copyChanges = options.copyChanges; // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - this.__currentActiveRelations = source.__currentActiveRelations; + copiedModel.__currentActiveRelations = source.__currentActiveRelations; - this.__parseRelations(source.__activeRelations) + copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model - this.parse(source.toJS()) + copiedModel.parse(source.toJS()); // Set only the changed attributes if (copyChanges) { - this._copyChanges(source) + copiedModel._copyChanges(source) } + + return copiedModel; } /** diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index eb6a01d..1e431ac 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1848,6 +1848,46 @@ test('copy (with changes)', () => { expect(customerCopyWithChanges.toBackendAll({ onlyChanges: true })).toEqual(customer.toBackendAll({ onlyChanges: true })) }); +test('copy (with changes without instantiating model)', () => { + const customer = new Customer(null, { + relations: ['oldTowns.bestCook.workPlaces'], + }); + + customer.fromBackend({ + data: customersWithTownCookRestaurant.data, + repos: customersWithTownCookRestaurant.with, + relMapping: customersWithTownCookRestaurant.with_mapping, + }); + + + customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); + + const customerCopyWithChanges = customer.copy({copyChanges: true}) + + // Clone with changes should give the same toBackend result as the cloned object + expect(customerCopyWithChanges.toBackendAll({ onlyChanges: true })).toEqual(customer.toBackendAll({ onlyChanges: true })) +}); + +test('copy (without instantiating model)', () => { + const customer = new Customer(null, { + relations: ['oldTowns.bestCook.workPlaces'], + }); + + customer.fromBackend({ + data: customersWithTownCookRestaurant.data, + repos: customersWithTownCookRestaurant.with, + relMapping: customersWithTownCookRestaurant.with_mapping, + }); + + + customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); + + const customerCopyWithChanges = customer.copy() + + // Clone with changes should give the same toBackend result as the cloned object + expect(customerCopyWithChanges.toBackendAll({ onlyChanges: true })).toEqual(customer.toBackendAll({ onlyChanges: true })) +}); + test('copy (without changes)', () => { const customer = new Customer(null, { relations: ['oldTowns.bestCook.workPlaces'], From cbc7de0f90620d5f7b7a5568118ea6e50dbb52a8 Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 9 Mar 2021 18:21:41 +0100 Subject: [PATCH 09/45] Add build --- dist/mobx-spine.cjs.js | 31 ++++++++++++++++++++++++++----- dist/mobx-spine.es.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index a29aad3..ab9c5fa 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1164,6 +1164,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * Makes this model a copy of the specified model + * or returns a copy of the current model when no model to copy is given * It also clones the changes that were in the specified model. * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. * Cloning @@ -1174,23 +1175,43 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'copy', - value: function copy(source) { + value: function copy() { + var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; + var copiedModel = void 0; + // If our source is not a model it is 'probably' the options + if (source !== undefined && !(source instanceof Model)) { + options = source; + source = undefined; + } + + // Make sure that we have the correct model + if (source === undefined) { + source = this; + copiedModel = new source.constructor(); + } else if (this.constructor !== source.constructor) { + copiedModel = new source.constructor(); + } else { + copiedModel = this; + } + var copyChanges = options.copyChanges; // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - this.__currentActiveRelations = source.__currentActiveRelations; + copiedModel.__currentActiveRelations = source.__currentActiveRelations; - this.__parseRelations(source.__activeRelations); + copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model - this.parse(source.toJS()); + copiedModel.parse(source.toJS()); // Set only the changed attributes if (copyChanges) { - this._copyChanges(source); + copiedModel._copyChanges(source); } + + return copiedModel; } /** diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 20df20b..371f18d 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1158,6 +1158,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * Makes this model a copy of the specified model + * or returns a copy of the current model when no model to copy is given * It also clones the changes that were in the specified model. * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. * Cloning @@ -1168,23 +1169,43 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'copy', - value: function copy(source) { + value: function copy() { + var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; + var copiedModel = void 0; + // If our source is not a model it is 'probably' the options + if (source !== undefined && !(source instanceof Model)) { + options = source; + source = undefined; + } + + // Make sure that we have the correct model + if (source === undefined) { + source = this; + copiedModel = new source.constructor(); + } else if (this.constructor !== source.constructor) { + copiedModel = new source.constructor(); + } else { + copiedModel = this; + } + var copyChanges = options.copyChanges; // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - this.__currentActiveRelations = source.__currentActiveRelations; + copiedModel.__currentActiveRelations = source.__currentActiveRelations; - this.__parseRelations(source.__activeRelations); + copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model - this.parse(source.toJS()); + copiedModel.parse(source.toJS()); // Set only the changed attributes if (copyChanges) { - this._copyChanges(source); + copiedModel._copyChanges(source); } + + return copiedModel; } /** From 1d2980a8c22c4dc32808f543266811293efed8ef Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 17 Mar 2021 16:39:08 +0100 Subject: [PATCH 10/45] Add new build number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 567b96f..48bda4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.0", + "version": "0.28.1", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From aa2d4bb57a7dc24bd70070ef410ba8d74f5718eb Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 29 Mar 2021 13:28:25 +0200 Subject: [PATCH 11/45] Add check method to model that does a dry run save to check for validation errors Ref T30324 --- dist/mobx-spine.cjs.js | 88 ++++++++++++++++++++++++++++++------------ dist/mobx-spine.es.js | 88 ++++++++++++++++++++++++++++++------------ package.json | 2 +- src/Model.js | 80 ++++++++++++++++++++++++++------------ src/__tests__/Model.js | 42 ++++++++++++++++++++ 5 files changed, 226 insertions(+), 74 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index ab9c5fa..a042db0 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1520,6 +1520,23 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function saveFiles() { return Promise.all(this.fileFields().filter(this.fieldFilter).map(this.saveFile)); } + + /** + * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal save request, example: {onlyChanges: true} + */ + + }, { + key: 'validate', + value: function validate() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate option + options.validate = true; + this.save(options); + } }, { key: 'save', value: function save() { @@ -1539,14 +1556,17 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { isNew: this.isNew, requestOptions: lodash.omit(options, 'url', 'data', 'mapData') }).then(mobx.action(function (res) { - _this12.saveFromBackend(_extends({}, res, { - data: lodash.omit(res.data, _this12.fileFields().map(camelToSnake)) - })); - _this12.clearUserFieldChanges(); - return _this12.saveFiles().then(function () { - _this12.clearUserFileChanges(); - return Promise.resolve(res); - }); + // Only update the model when we are actually trying to save + if (!options.validate) { + _this12.saveFromBackend(_extends({}, res, { + data: lodash.omit(res.data, _this12.fileFields().map(camelToSnake)) + })); + _this12.clearUserFieldChanges(); + return _this12.saveFiles().then(function () { + _this12.clearUserFileChanges(); + return Promise.resolve(res); + }); + } })).catch(mobx.action(function (err) { if (err.valErrors) { _this12.parseValidationErrors(err.valErrors); @@ -1630,6 +1650,23 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return Promise.all(promises); } + + /** + * Validates a model and relations by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} + */ + + }, { + key: 'validateAll', + value: function validateAll() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate option + options.validate = true; + this.saveAll(options); + } }, { key: 'saveAll', value: function saveAll() { @@ -1649,28 +1686,31 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }), requestOptions: lodash.omit(options, 'relations', 'data', 'mapData') }).then(mobx.action(function (res) { - _this13.saveFromBackend(res); - _this13.clearUserFieldChanges(); - - forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); - - return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this13.clearUserFileChanges(); + // Only update the models if we are actually trying to save + if (!options.validate) { + _this13.saveFromBackend(res); + _this13.clearUserFieldChanges(); forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { if (relation instanceof Model) { - relation.clearUserFileChanges(); + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); } }); - return res; - }); + return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + _this13.clearUserFileChanges(); + + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFileChanges(); + } + }); + + return res; + }); + } })).catch(mobx.action(function (err) { if (err.valErrors) { _this13.parseValidationErrors(err.valErrors); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 371f18d..65f1742 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1514,6 +1514,23 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function saveFiles() { return Promise.all(this.fileFields().filter(this.fieldFilter).map(this.saveFile)); } + + /** + * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal save request, example: {onlyChanges: true} + */ + + }, { + key: 'validate', + value: function validate() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate option + options.validate = true; + this.save(options); + } }, { key: 'save', value: function save() { @@ -1533,14 +1550,17 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { isNew: this.isNew, requestOptions: omit(options, 'url', 'data', 'mapData') }).then(action(function (res) { - _this12.saveFromBackend(_extends({}, res, { - data: omit(res.data, _this12.fileFields().map(camelToSnake)) - })); - _this12.clearUserFieldChanges(); - return _this12.saveFiles().then(function () { - _this12.clearUserFileChanges(); - return Promise.resolve(res); - }); + // Only update the model when we are actually trying to save + if (!options.validate) { + _this12.saveFromBackend(_extends({}, res, { + data: omit(res.data, _this12.fileFields().map(camelToSnake)) + })); + _this12.clearUserFieldChanges(); + return _this12.saveFiles().then(function () { + _this12.clearUserFileChanges(); + return Promise.resolve(res); + }); + } })).catch(action(function (err) { if (err.valErrors) { _this12.parseValidationErrors(err.valErrors); @@ -1624,6 +1644,23 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return Promise.all(promises); } + + /** + * Validates a model and relations by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} + */ + + }, { + key: 'validateAll', + value: function validateAll() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate option + options.validate = true; + this.saveAll(options); + } }, { key: 'saveAll', value: function saveAll() { @@ -1643,28 +1680,31 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }), requestOptions: omit(options, 'relations', 'data', 'mapData') }).then(action(function (res) { - _this13.saveFromBackend(res); - _this13.clearUserFieldChanges(); - - forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); - - return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this13.clearUserFileChanges(); + // Only update the models if we are actually trying to save + if (!options.validate) { + _this13.saveFromBackend(res); + _this13.clearUserFieldChanges(); forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { if (relation instanceof Model) { - relation.clearUserFileChanges(); + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); } }); - return res; - }); + return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + _this13.clearUserFileChanges(); + + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFileChanges(); + } + }); + + return res; + }); + } })).catch(action(function (err) { if (err.valErrors) { _this13.parseValidationErrors(err.valErrors); diff --git a/package.json b/package.json index 48bda4c..e5c4b76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.1", + "version": "0.28.2", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", diff --git a/src/Model.js b/src/Model.js index 4ee9c21..2c1767d 100644 --- a/src/Model.js +++ b/src/Model.js @@ -749,6 +749,18 @@ export default class Model { ); } + /** + * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal save request, example: {onlyChanges: true} + */ + validate(options = {}){ + // Add the validate option + options.validate = true; + this.save(options); + } + @action save(options = {}) { this.clearValidationErrors(); @@ -766,15 +778,18 @@ export default class Model { requestOptions: omit(options, 'url', 'data', 'mapData') }) .then(action(res => { - this.saveFromBackend({ - ...res, - data: omit(res.data, this.fileFields().map(camelToSnake)), - }); - this.clearUserFieldChanges(); - return this.saveFiles().then(() => { - this.clearUserFileChanges(); - return Promise.resolve(res); - }); + // Only update the model when we are actually trying to save + if (!options.validate) { + this.saveFromBackend({ + ...res, + data: omit(res.data, this.fileFields().map(camelToSnake)), + }); + this.clearUserFieldChanges(); + return this.saveFiles().then(() => { + this.clearUserFileChanges(); + return Promise.resolve(res); + }); + } })) .catch( action(err => { @@ -843,6 +858,18 @@ export default class Model { return Promise.all(promises); } + /** + * Validates a model and relations by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} + */ + validateAll(options = {}){ + // Add the validate option + options.validate = true; + this.saveAll(options); + } + @action saveAll(options = {}) { this.clearValidationErrors(); @@ -860,28 +887,31 @@ export default class Model { requestOptions: omit(options, 'relations', 'data', 'mapData'), }) .then(action(res => { - this.saveFromBackend(res); - this.clearUserFieldChanges(); - - forNestedRelations(this, relationsToNestedKeys(options.relations || []), relation => { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); - - return this.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(() => { - this.clearUserFileChanges(); + // Only update the models if we are actually trying to save + if (!options.validate) { + this.saveFromBackend(res); + this.clearUserFieldChanges(); forNestedRelations(this, relationsToNestedKeys(options.relations || []), relation => { if (relation instanceof Model) { - relation.clearUserFileChanges(); + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); } }); - return res; - }); + return this.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(() => { + this.clearUserFileChanges(); + + forNestedRelations(this, relationsToNestedKeys(options.relations || []), relation => { + if (relation instanceof Model) { + relation.clearUserFileChanges(); + } + }); + + return res; + }); + } })) .catch( action(err => { diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index 1e431ac..fee1439 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1909,4 +1909,46 @@ test('copy (without changes)', () => { expect(customerCopyNoChanges.toBackendAll({onlyChanges: false})).toEqual(customer.toBackendAll({onlyChanges: false})) }); +// test('validate', () => { +// const customer = new Customer(null, { +// relations: ['oldTowns.bestCook.workPlaces'], +// }); +// +// customer.fromBackend({ +// data: customersWithTownCookRestaurant.data, +// repos: customersWithTownCookRestaurant.with, +// relMapping: customersWithTownCookRestaurant.with_mapping, +// }); +// +// customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); +// +// const customerCopyNoChanges = new Customer(); +// customerCopyNoChanges.copy(customer, {copyChanges: true}) +// +// +// // Clone without changes should give the same toBackend result as the cloned object when only changes is false +// expect(customerCopyNoChanges.toBackendAll({onlyChanges: false})).toEqual(customer.toBackendAll({onlyChanges: false})) +// }); +// +// test('validateAll', () => { +// const customer = new Customer(null, { +// relations: ['oldTowns.bestCook.workPlaces'], +// }); +// +// customer.fromBackend({ +// data: customersWithTownCookRestaurant.data, +// repos: customersWithTownCookRestaurant.with, +// relMapping: customersWithTownCookRestaurant.with_mapping, +// }); +// +// customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); +// +// const customerCopyNoChanges = new Customer(); +// customerCopyNoChanges.copy(customer, {copyChanges: true}) +// +// +// // Clone without changes should give the same toBackend result as the cloned object when only changes is false +// expect(customerCopyNoChanges.toBackendAll({onlyChanges: false})).toEqual(customer.toBackendAll({onlyChanges: false})) +// }); + From 309b7264a95cc89b132736c4bf956c381deefcf9 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 29 Mar 2021 14:14:27 +0200 Subject: [PATCH 12/45] Return request, so you can check if it was successful Ref T30324 --- dist/mobx-spine.cjs.js | 4 ++-- dist/mobx-spine.es.js | 4 ++-- src/Model.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index a042db0..dafbfa0 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1535,7 +1535,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Add the validate option options.validate = true; - this.save(options); + return this.save(options); } }, { key: 'save', @@ -1665,7 +1665,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Add the validate option options.validate = true; - this.saveAll(options); + return this.saveAll(options); } }, { key: 'saveAll', diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 65f1742..26f6663 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1529,7 +1529,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Add the validate option options.validate = true; - this.save(options); + return this.save(options); } }, { key: 'save', @@ -1659,7 +1659,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Add the validate option options.validate = true; - this.saveAll(options); + return this.saveAll(options); } }, { key: 'saveAll', diff --git a/src/Model.js b/src/Model.js index 2c1767d..dba7592 100644 --- a/src/Model.js +++ b/src/Model.js @@ -758,7 +758,7 @@ export default class Model { validate(options = {}){ // Add the validate option options.validate = true; - this.save(options); + return this.save(options); } @action @@ -867,7 +867,7 @@ export default class Model { validateAll(options = {}){ // Add the validate option options.validate = true; - this.saveAll(options); + return this.saveAll(options); } @action From 6bb69f8635ed5e6fb459d1584e5ab3abce618669 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 29 Mar 2021 14:56:31 +0200 Subject: [PATCH 13/45] Proper way of adding header to model validation request Ref T30324 --- dist/mobx-spine.cjs.js | 18 +++++++++++++----- dist/mobx-spine.es.js | 18 +++++++++++++----- src/Model.js | 18 +++++++++++++----- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index dafbfa0..c243287 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1533,8 +1533,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function validate() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - // Add the validate option - options.validate = true; + // Add the validate parameter + if (options.params) { + options.params = { validate: true }; + } else { + options.params.validate = true; + } return this.save(options); } }, { @@ -1557,7 +1561,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { requestOptions: lodash.omit(options, 'url', 'data', 'mapData') }).then(mobx.action(function (res) { // Only update the model when we are actually trying to save - if (!options.validate) { + if (!options.params || !options.params.validate) { _this12.saveFromBackend(_extends({}, res, { data: lodash.omit(res.data, _this12.fileFields().map(camelToSnake)) })); @@ -1664,7 +1668,11 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Add the validate option - options.validate = true; + if (options.params) { + options.params = { validate: true }; + } else { + options.params.validate = true; + } return this.saveAll(options); } }, { @@ -1687,7 +1695,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { requestOptions: lodash.omit(options, 'relations', 'data', 'mapData') }).then(mobx.action(function (res) { // Only update the models if we are actually trying to save - if (!options.validate) { + if (!options.params || !options.params.validate) { _this13.saveFromBackend(res); _this13.clearUserFieldChanges(); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 26f6663..704ebcb 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1527,8 +1527,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function validate() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - // Add the validate option - options.validate = true; + // Add the validate parameter + if (options.params) { + options.params = { validate: true }; + } else { + options.params.validate = true; + } return this.save(options); } }, { @@ -1551,7 +1555,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { requestOptions: omit(options, 'url', 'data', 'mapData') }).then(action(function (res) { // Only update the model when we are actually trying to save - if (!options.validate) { + if (!options.params || !options.params.validate) { _this12.saveFromBackend(_extends({}, res, { data: omit(res.data, _this12.fileFields().map(camelToSnake)) })); @@ -1658,7 +1662,11 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Add the validate option - options.validate = true; + if (options.params) { + options.params = { validate: true }; + } else { + options.params.validate = true; + } return this.saveAll(options); } }, { @@ -1681,7 +1689,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { requestOptions: omit(options, 'relations', 'data', 'mapData') }).then(action(function (res) { // Only update the models if we are actually trying to save - if (!options.validate) { + if (!options.params || !options.params.validate) { _this13.saveFromBackend(res); _this13.clearUserFieldChanges(); diff --git a/src/Model.js b/src/Model.js index dba7592..e49d290 100644 --- a/src/Model.js +++ b/src/Model.js @@ -756,8 +756,12 @@ export default class Model { * @param options - same as for a normal save request, example: {onlyChanges: true} */ validate(options = {}){ - // Add the validate option - options.validate = true; + // Add the validate parameter + if (options.params){ + options.params = { validate: true }; + } else { + options.params.validate = true + } return this.save(options); } @@ -779,7 +783,7 @@ export default class Model { }) .then(action(res => { // Only update the model when we are actually trying to save - if (!options.validate) { + if (!options.params || !options.params.validate) { this.saveFromBackend({ ...res, data: omit(res.data, this.fileFields().map(camelToSnake)), @@ -866,7 +870,11 @@ export default class Model { */ validateAll(options = {}){ // Add the validate option - options.validate = true; + if (options.params){ + options.params = { validate: true }; + } else { + options.params.validate = true + } return this.saveAll(options); } @@ -888,7 +896,7 @@ export default class Model { }) .then(action(res => { // Only update the models if we are actually trying to save - if (!options.validate) { + if (!options.params || !options.params.validate) { this.saveFromBackend(res); this.clearUserFieldChanges(); From 4bd6b17c7401f6fa8ef9ef03ad8d8c33593de1ee Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 29 Mar 2021 15:01:10 +0200 Subject: [PATCH 14/45] Proper way of adding header to model validation request Ref T30324 --- dist/mobx-spine.cjs.js | 8 ++++---- dist/mobx-spine.es.js | 8 ++++---- src/Model.js | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index c243287..0992bf1 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1535,9 +1535,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Add the validate parameter if (options.params) { - options.params = { validate: true }; - } else { options.params.validate = true; + } else { + options.params = { validate: true }; } return this.save(options); } @@ -1669,9 +1669,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Add the validate option if (options.params) { - options.params = { validate: true }; - } else { options.params.validate = true; + } else { + options.params = { validate: true }; } return this.saveAll(options); } diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 704ebcb..79140ec 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1529,9 +1529,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Add the validate parameter if (options.params) { - options.params = { validate: true }; - } else { options.params.validate = true; + } else { + options.params = { validate: true }; } return this.save(options); } @@ -1663,9 +1663,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Add the validate option if (options.params) { - options.params = { validate: true }; - } else { options.params.validate = true; + } else { + options.params = { validate: true }; } return this.saveAll(options); } diff --git a/src/Model.js b/src/Model.js index e49d290..9f1baad 100644 --- a/src/Model.js +++ b/src/Model.js @@ -758,9 +758,9 @@ export default class Model { validate(options = {}){ // Add the validate parameter if (options.params){ - options.params = { validate: true }; - } else { options.params.validate = true + } else { + options.params = { validate: true }; } return this.save(options); } @@ -871,9 +871,9 @@ export default class Model { validateAll(options = {}){ // Add the validate option if (options.params){ - options.params = { validate: true }; - } else { options.params.validate = true + } else { + options.params = { validate: true }; } return this.saveAll(options); } From 3797aad791f16f90857e066f3092bda31e2c927a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 12:56:45 +0200 Subject: [PATCH 15/45] Fix relations setting --- dist/mobx-spine.cjs.js | 4 ++-- dist/mobx-spine.es.js | 4 ++-- src/Model.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index dafbfa0..9c50583 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1189,9 +1189,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Make sure that we have the correct model if (source === undefined) { source = this; - copiedModel = new source.constructor(); + copiedModel = new source.constructor({ relations: source.__activeRelations }); } else if (this.constructor !== source.constructor) { - copiedModel = new source.constructor(); + copiedModel = new source.constructor({ relations: source.__activeRelations }); } else { copiedModel = this; } diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 26f6663..86b8dbe 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1183,9 +1183,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Make sure that we have the correct model if (source === undefined) { source = this; - copiedModel = new source.constructor(); + copiedModel = new source.constructor({ relations: source.__activeRelations }); } else if (this.constructor !== source.constructor) { - copiedModel = new source.constructor(); + copiedModel = new source.constructor({ relations: source.__activeRelations }); } else { copiedModel = this; } diff --git a/src/Model.js b/src/Model.js index dba7592..6594d17 100644 --- a/src/Model.js +++ b/src/Model.js @@ -434,9 +434,9 @@ export default class Model { // Make sure that we have the correct model if (source === undefined){ source = this; - copiedModel = new source.constructor(); + copiedModel = new source.constructor({relations: source.__activeRelations}); } else if (this.constructor !== source.constructor) { - copiedModel = new source.constructor(); + copiedModel = new source.constructor({relations: source.__activeRelations}); } else { copiedModel = this; } From d93c76fabf34b9faabb08f01e6ceea87c5fac11a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 13:10:06 +0200 Subject: [PATCH 16/45] Fix relations setting --- dist/mobx-spine.cjs.js | 28 +++++++++++++++------------- dist/mobx-spine.es.js | 28 +++++++++++++++------------- src/Model.js | 29 +++++++++++++++-------------- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 9c50583..b884928 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1200,7 +1200,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - copiedModel.__currentActiveRelations = source.__currentActiveRelations; + copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model @@ -1229,7 +1229,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Maintain the relations after copy this.__activeRelations = source.__activeRelations; - this.__currentActiveRelations = source.__currentActiveRelations; + this.__activeCurrentRelations = source.__activeCurrentRelations; // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { @@ -1243,18 +1243,20 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { _this6.setInput(changedAttribute, source[changedAttribute]); }); } - - // Set the changes for all related models with changes - source.__activeRelations.forEach(function (relation) { - if (relation && source[relation]) { - if (source[relation].hasUserChanges) { - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); - }); + // Undefined safety + if (source.__activeCurrentRelations.length > 0) { + // Set the changes for all related models with changes + source.__activeCurrentRelations.forEach(function (relation) { + if (relation && source[relation]) { + if (source[relation].hasUserChanges) { + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); + }); + } } - } - }); + }); + } } }, { key: 'toJS', diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 86b8dbe..c52a2c3 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1194,7 +1194,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - copiedModel.__currentActiveRelations = source.__currentActiveRelations; + copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model @@ -1223,7 +1223,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Maintain the relations after copy this.__activeRelations = source.__activeRelations; - this.__currentActiveRelations = source.__currentActiveRelations; + this.__activeCurrentRelations = source.__activeCurrentRelations; // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { @@ -1237,18 +1237,20 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { _this6.setInput(changedAttribute, source[changedAttribute]); }); } - - // Set the changes for all related models with changes - source.__activeRelations.forEach(function (relation) { - if (relation && source[relation]) { - if (source[relation].hasUserChanges) { - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); - }); + // Undefined safety + if (source.__activeCurrentRelations.length > 0) { + // Set the changes for all related models with changes + source.__activeCurrentRelations.forEach(function (relation) { + if (relation && source[relation]) { + if (source[relation].hasUserChanges) { + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); + }); + } } - } - }); + }); + } } }, { key: 'toJS', diff --git a/src/Model.js b/src/Model.js index 6594d17..0332af8 100644 --- a/src/Model.js +++ b/src/Model.js @@ -445,7 +445,7 @@ export default class Model { // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - copiedModel.__currentActiveRelations = source.__currentActiveRelations; + copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model @@ -470,7 +470,7 @@ export default class Model { _copyChanges(source, store) { // Maintain the relations after copy this.__activeRelations = source.__activeRelations; - this.__currentActiveRelations = source.__currentActiveRelations; + this.__activeCurrentRelations = source.__activeCurrentRelations; // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { @@ -484,19 +484,20 @@ export default class Model { this.setInput(changedAttribute, source[changedAttribute]) }) } - - - // Set the changes for all related models with changes - source.__activeRelations.forEach((relation) => { - if (relation && source[relation]) { - if (source[relation].hasUserChanges) { - // Set the changes for all related models with changes - source[relation].models.forEach((relatedModel,index) => { - this[relation].models[index]._copyChanges(relatedModel, this[relation]); - }); + // Undefined safety + if (source.__activeCurrentRelations.length > 0) { + // Set the changes for all related models with changes + source.__activeCurrentRelations.forEach((relation) => { + if (relation && source[relation]) { + if (source[relation].hasUserChanges) { + // Set the changes for all related models with changes + source[relation].models.forEach((relatedModel, index) => { + this[relation].models[index]._copyChanges(relatedModel, this[relation]); + }); + } } - } - }); + }); + } } toJS() { From e46b198ca2c12e45269f48fb2ec113400a16ce50 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 13:13:23 +0200 Subject: [PATCH 17/45] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5c4b76..d1370ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.2", + "version": "0.28.3", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From dff07d00eacceec668876e960fdc5421adab78b6 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 13:24:15 +0200 Subject: [PATCH 18/45] Added fix for copy where related items are models instead of stores --- dist/mobx-spine.cjs.js | 2334 ---------------------------------------- dist/mobx-spine.es.js | 2324 --------------------------------------- src/Model.js | 13 +- 3 files changed, 9 insertions(+), 4662 deletions(-) delete mode 100644 dist/mobx-spine.cjs.js delete mode 100644 dist/mobx-spine.es.js diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js deleted file mode 100644 index 756af2e..0000000 --- a/dist/mobx-spine.cjs.js +++ /dev/null @@ -1,2334 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, '__esModule', { value: true }); - -function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } - -var mobx = require('mobx'); -var lodash = require('lodash'); -var axios = _interopDefault(require('axios')); -var moment = _interopDefault(require('moment')); -var luxon = require('luxon'); - -function invariant(condition) { - var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Illegal state'; - - if (!condition) { - throw new Error('[mobx-spine] ' + message); - } -} - -// lodash's `snakeCase` method removes dots from the string; this breaks mobx-spine -function camelToSnake(s) { - return s.replace(/([A-Z])/g, function ($1) { - return '_' + $1.toLowerCase(); - }); -} - -// lodash's `camelCase` method removes dots from the string; this breaks mobx-spine -function snakeToCamel(s) { - if (s.startsWith('_')) { - return s; - } - return s.replace(/_\w/g, function (m) { - return m[1].toUpperCase(); - }); -} - -// ['kind.breed', 'owner'] => { 'owner': {}, 'kind': {'breed': {}}} -function relationsToNestedKeys(relations) { - var nestedRelations = {}; - - relations.forEach(function (rel) { - var current = nestedRelations; - var components = rel.split('.'); - var len = components.length; - - for (var i = 0; i < len; ++i) { - var head = components[i]; - if (current[head] === undefined) { - current[head] = {}; - } - current = current[head]; - } - }); - - return nestedRelations; -} - -// Use output of relationsToNestedKeys to iterate each relation, fn is called on each model and store. -function forNestedRelations(model, nestedRelations, fn) { - Object.keys(nestedRelations).forEach(function (key) { - if (Object.keys(nestedRelations[key]).length > 0) { - if (model[key].forEach) { - model[key].forEach(function (m) { - forNestedRelations(m, nestedRelations[key], fn); - }); - - fn(model); - } else { - forNestedRelations(model[key], nestedRelations[key], fn); - } - } - - if (model[key].forEach) { - model[key].forEach(fn); - } - - fn(model[key]); - }); -} - -var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -}; - -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; - }; -}(); - -var defineProperty = function (obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - - return obj; -}; - -var _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; -}; - -var objectWithoutProperties = function (obj, keys) { - var target = {}; - - for (var i in obj) { - if (keys.indexOf(i) >= 0) continue; - if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; - target[i] = obj[i]; - } - - return target; -}; - -var _class, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _class2, _temp; - -function _initDefineProp(target, property, descriptor, context) { - if (!descriptor) return; - Object.defineProperty(target, property, { - enumerable: descriptor.enumerable, - configurable: descriptor.configurable, - writable: descriptor.writable, - value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 - }); -} - -function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { - var desc = {}; - Object['ke' + 'ys'](descriptor).forEach(function (key) { - desc[key] = descriptor[key]; - }); - desc.enumerable = !!desc.enumerable; - desc.configurable = !!desc.configurable; - - if ('value' in desc || desc.initializer) { - desc.writable = true; - } - - desc = decorators.slice().reverse().reduce(function (desc, decorator) { - return decorator(target, property, desc) || desc; - }, desc); - - if (context && desc.initializer !== void 0) { - desc.value = desc.initializer ? desc.initializer.call(context) : void 0; - desc.initializer = undefined; - } - - if (desc.initializer === void 0) { - Object['define' + 'Property'](target, property, desc); - desc = null; - } - - return desc; -} -var AVAILABLE_CONST_OPTIONS = ['relations', 'limit', 'comparator', 'params', 'repository']; - -var Store = (_class = (_temp = _class2 = function () { - createClass(Store, [{ - key: 'url', - value: function url() { - // Try to auto-generate the URL. - var bname = this.constructor.backendResourceName; - if (bname) { - return '/' + bname + '/'; - } - return null; - } - // The set of models has changed - - // Holds the fetch parameters - - }, { - key: 'initialize', - - - // Empty function, but can be overridden if you want to do something after initializing the model. - value: function initialize() {} - }, { - key: 'isLoading', - get: function get$$1() { - return this.__pendingRequestCount > 0; - } - }, { - key: 'length', - get: function get$$1() { - return this.models.length; - } - }, { - key: 'backendResourceName', - set: function set$$1(v) { - invariant(false, '`backendResourceName` should be a static property on the store.'); - } - }]); - - function Store() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - classCallCheck(this, Store); - - _initDefineProp(this, 'models', _descriptor, this); - - _initDefineProp(this, 'params', _descriptor2, this); - - _initDefineProp(this, '__pendingRequestCount', _descriptor3, this); - - _initDefineProp(this, '__setChanged', _descriptor4, this); - - _initDefineProp(this, '__state', _descriptor5, this); - - this.__activeRelations = []; - this.Model = null; - this.api = null; - - invariant(lodash.isPlainObject(options), 'Store only accepts an object with options. Chain `.parse(data)` to add models.'); - lodash.forIn(options, function (value, option) { - invariant(AVAILABLE_CONST_OPTIONS.includes(option), 'Unknown option passed to store: ' + option); - }); - this.__repository = options.repository; - if (options.relations) { - this.__parseRelations(options.relations); - } - if (options.limit !== undefined) { - this.setLimit(options.limit); - } - if (options.comparator) { - this.comparator = options.comparator; - } - if (options.params) { - this.params = options.params; - } - this.initialize(); - } - - createClass(Store, [{ - key: '__parseRelations', - value: function __parseRelations(activeRelations) { - this.__activeRelations = activeRelations; - } - }, { - key: '__getApi', - value: function __getApi() { - invariant(this.api, 'You are trying to perform a API request without an `api` property defined on the store.'); - invariant(lodash.result(this, 'url'), 'You are trying to perform a API request without an `url` property defined on the store.'); - return this.api; - } - }, { - key: 'fromBackend', - value: function fromBackend(_ref) { - var _this = this; - - var data = _ref.data, - repos = _ref.repos, - relMapping = _ref.relMapping, - reverseRelMapping = _ref.reverseRelMapping; - - invariant(data, 'Backend error. Data is not set. HINT: DID YOU FORGET THE M2M again?'); - - this.models.replace(data.map(function (record) { - // TODO: I'm not happy at all about how this looks. - // We'll need to finetune some things, but hey, for now it works. - var model = _this._newModel(); - model.fromBackend({ - data: record, - repos: repos, - relMapping: relMapping, - reverseRelMapping: reverseRelMapping - }); - return model; - })); - this.sort(); - } - }, { - key: '_newModel', - value: function _newModel() { - var model = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - - return new this.Model(model, { - store: this, - relations: this.__activeRelations - }); - } - }, { - key: 'sort', - value: function sort() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - invariant(lodash.isPlainObject(options), 'Expecting a plain object for options.'); - if (!this.comparator) { - return this; - } - if (typeof this.comparator === 'string') { - this.models.replace(this.sortBy(this.comparator)); - } else { - this.models.replace(this.models.slice().sort(this.comparator)); - } - return this; - } - }, { - key: 'parse', - value: function parse(models) { - invariant(lodash.isArray(models), 'Parameter supplied to `parse()` is not an array, got: ' + JSON.stringify(models)); - // Parse does not mutate __setChanged, as it is used in - // fromBackend in the model... - this.models.replace(models.map(this._newModel.bind(this))); - this.sort(); - - return this; - } - }, { - key: 'parseValidationErrors', - value: function parseValidationErrors(valErrors) { - this.each(function (model) { - model.parseValidationErrors(valErrors); - }); - } - }, { - key: 'clearValidationErrors', - value: function clearValidationErrors() { - this.each(function (model) { - model.clearValidationErrors(); - }); - } - }, { - key: 'add', - value: function add(models) { - var _this2 = this; - - var singular = !lodash.isArray(models); - models = singular ? [models] : models.slice(); - - var modelInstances = models.map(this._newModel.bind(this)); - - modelInstances.forEach(function (modelInstance) { - var primaryValue = modelInstance[_this2.Model.primaryKey]; - invariant(!primaryValue || !_this2.get(primaryValue), 'A model with the same primary key value "' + primaryValue + '" already exists in this store.'); - _this2.__setChanged = true; - _this2.models.push(modelInstance); - }); - this.sort(); - - return singular ? modelInstances[0] : modelInstances; - } - }, { - key: 'remove', - value: function remove(models) { - var _this3 = this; - - var singular = !lodash.isArray(models); - models = singular ? [models] : models.slice(); - - models.forEach(function (model) { - return _this3.models.remove(model); - }); - if (models.length > 0) { - this.__setChanged = true; - } - return models; - } - }, { - key: 'removeById', - value: function removeById(ids) { - var _this4 = this; - - var singular = !lodash.isArray(ids); - ids = singular ? [ids] : ids.slice(); - invariant(!ids.some(isNaN), 'Cannot remove a model by id that is not a number: ' + JSON.stringify(ids)); - - var models = ids.map(function (id) { - return _this4.get(id); - }); - - models.forEach(function (model) { - if (model) { - _this4.models.remove(model); - _this4.__setChanged = true; - } - }); - - return models; - } - }, { - key: 'clear', - value: function clear() { - var length = this.models.length; - this.models.clear(); - - if (length > 0) { - this.__setChanged = true; - } - } - }, { - key: 'buildFetchData', - value: function buildFetchData(options) { - return Object.assign(this.__getApi().buildFetchStoreParams(this), this.params, options.data); - } - }, { - key: 'fetch', - value: function fetch() { - var _this5 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - - var data = this.buildFetchData(options); - var promise = this.wrapPendingRequestCount(this.__getApi().fetchStore({ - url: options.url || lodash.result(this, 'url'), - data: data, - requestOptions: lodash.omit(options, 'data') - }).then(mobx.action(function (res) { - _this5.__state.totalRecords = res.totalRecords; - _this5.fromBackend(res); - - return res.response; - }))); - - return promise; - } - }, { - key: '__parseNewIds', - value: function __parseNewIds(idMaps) { - this.each(function (model) { - return model.__parseNewIds(idMaps); - }); - } - }, { - key: 'toJS', - value: function toJS() { - return this.models.map(function (model) { - return model.toJS(); - }); - } - - // Methods for pagination. - - }, { - key: 'getPageOffset', - value: function getPageOffset() { - return (this.__state.currentPage - 1) * this.__state.limit; - } - }, { - key: 'setLimit', - value: function setLimit(limit) { - invariant(!limit || Number.isInteger(limit), 'Page limit should be a number or falsy value.'); - this.__state.limit = limit || null; - } - }, { - key: 'getNextPage', - value: function getNextPage() { - invariant(this.hasNextPage, 'There is no next page.'); - this.__state.currentPage += 1; - return this.fetch(); - } - }, { - key: 'getPreviousPage', - value: function getPreviousPage() { - invariant(this.hasPreviousPage, 'There is no previous page.'); - this.__state.currentPage -= 1; - return this.fetch(); - } - }, { - key: 'setPage', - value: function setPage() { - var page = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - invariant(Number.isInteger(page) && page >= 1, 'Page should be a number above 1.'); - this.__state.currentPage = page; - if (options.fetch === undefined || options.fetch) { - return this.fetch(); - } - invariant( - // Always allow to go to page 1. - page <= (this.totalPages || 1), 'Page should be between 1 and ' + this.totalPages + '.'); - return Promise.resolve(); - } - }, { - key: 'clearSetChanges', - value: function clearSetChanges() { - this.__setChanged = false; - } - }, { - key: 'toBackendAll', - value: function toBackendAll() { - var _this6 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var relevantModels = options.onlyChanges ? this.models.filter(function (model) { - return model.isNew || model.hasUserChanges; - }) : this.models; - var modelData = relevantModels.map(function (model) { - return model.toBackendAll(options); - }); - - var data = []; - var relations = {}; - - modelData.forEach(function (model) { - data = data.concat(model.data); - lodash.forIn(model.relations, function (relModel, key) { - relations[key] = relations[key] ? relations[key].concat(relModel) : relModel; - // TODO: this primaryKey is not the primaryKey of the relation we're de-duplicating... - relations[key] = lodash.uniqBy(relations[key], _this6.Model.primaryKey); - }); - }); - - return { data: data, relations: relations }; - } - - // Create a new instance of this store with a predicate applied. - // This new store will be automatically kept in-sync with all models that adhere to the predicate. - - }, { - key: 'virtualStore', - value: function virtualStore(_ref2) { - var _this7 = this; - - var filter = _ref2.filter, - comparator = _ref2.comparator; - - var store = new this.constructor({ - relations: this.__activeRelations, - comparator: comparator - }); - - // Oh gawd MobX is so awesome. - var events = mobx.autorun(function () { - var models = _this7.filter(filter); - store.models.replace(models); - store.sort(); - - // When the parent store is busy, make sure the virtual store is - // also busy. - store.__pendingRequestCount = _this7.__pendingRequestCount; - }); - - store.unsubscribeVirtualStore = events; - - return store; - } - - // Helper methods to read models. - - }, { - key: 'get', - value: function get$$1(id) { - // The id can be defined as a string or int, but we want it to work in both cases. - return this.models.find(function (model) { - return model[model.constructor.primaryKey] == id; - } // eslint-disable-line eqeqeq - ); - } - }, { - key: 'getByIds', - value: function getByIds(ids) { - return this.models.filter(function (model) { - var id = model[model.constructor.primaryKey]; - return ids.includes(id) || ids.includes('' + id); - }); - } - }, { - key: 'map', - value: function map(predicate) { - return lodash.map(this.models, predicate); - } - }, { - key: 'mapByPrimaryKey', - value: function mapByPrimaryKey() { - return this.map(this.Model.primaryKey); - } - }, { - key: 'filter', - value: function filter(predicate) { - return lodash.filter(this.models, predicate); - } - }, { - key: 'find', - value: function find(predicate) { - return lodash.find(this.models, predicate); - } - }, { - key: 'each', - value: function each(predicate) { - return this.models.forEach(predicate); - } - }, { - key: 'forEach', - value: function forEach(predicate) { - return this.models.forEach(predicate); - } - }, { - key: 'sortBy', - value: function sortBy(iteratees) { - return lodash.sortBy(this.models, iteratees); - } - }, { - key: 'at', - value: function at(index) { - var zeroLength = this.length - 1; - invariant(index <= zeroLength, 'Index ' + index + ' is out of bounds (max ' + zeroLength + ').'); - if (index < 0) { - index += this.length; - } - return this.models[index]; - } - }, { - key: 'wrapPendingRequestCount', - value: function wrapPendingRequestCount(promise) { - var _this8 = this; - - this.__pendingRequestCount++; - - return promise.then(function (res) { - _this8.__pendingRequestCount--; - return res; - }).catch(function (err) { - _this8.__pendingRequestCount--; - throw err; - }); - } - }, { - key: 'saveAllFiles', - value: function saveAllFiles() { - var relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var promises = []; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = this.models[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var model = _step.value; - - promises.push(model.saveAllFiles(relations)); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - return Promise.all(promises); - } - }, { - key: 'totalPages', - get: function get$$1() { - if (!this.__state.limit) { - return 0; - } - return Math.ceil(this.__state.totalRecords / this.__state.limit); - } - }, { - key: 'currentPage', - get: function get$$1() { - return this.__state.currentPage; - } - }, { - key: 'hasNextPage', - get: function get$$1() { - return this.__state.currentPage + 1 <= this.totalPages; - } - }, { - key: 'hasPreviousPage', - get: function get$$1() { - return this.__state.currentPage > 1; - } - }, { - key: 'hasUserChanges', - get: function get$$1() { - return this.hasSetChanges || this.models.some(function (m) { - return m.hasUserChanges; - }); - } - - // TODO: Maybe we can keep track of what got added and what got - // removed exactly. For now this should be enough. - - }, { - key: 'hasSetChanges', - get: function get$$1() { - return this.__setChanged; - } - }]); - return Store; -}(), _class2.backendResourceName = '', _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, 'models', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return []; - } -}), _descriptor2 = _applyDecoratedDescriptor(_class.prototype, 'params', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor3 = _applyDecoratedDescriptor(_class.prototype, '__pendingRequestCount', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return 0; - } -}), _descriptor4 = _applyDecoratedDescriptor(_class.prototype, '__setChanged', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return false; - } -}), _descriptor5 = _applyDecoratedDescriptor(_class.prototype, '__state', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return { - currentPage: 1, - limit: 25, - totalRecords: 0 - }; - } -}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype)), _class); - -var _class$1, _descriptor$1, _descriptor2$1, _descriptor3$1, _descriptor4$1, _descriptor5$1, _descriptor6, _descriptor7, _class2$1, _temp$1; - -function _initDefineProp$1(target, property, descriptor, context) { - if (!descriptor) return; - Object.defineProperty(target, property, { - enumerable: descriptor.enumerable, - configurable: descriptor.configurable, - writable: descriptor.writable, - value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 - }); -} - -function _applyDecoratedDescriptor$1(target, property, decorators, descriptor, context) { - var desc = {}; - Object['ke' + 'ys'](descriptor).forEach(function (key) { - desc[key] = descriptor[key]; - }); - desc.enumerable = !!desc.enumerable; - desc.configurable = !!desc.configurable; - - if ('value' in desc || desc.initializer) { - desc.writable = true; - } - - desc = decorators.slice().reverse().reduce(function (desc, decorator) { - return decorator(target, property, desc) || desc; - }, desc); - - if (context && desc.initializer !== void 0) { - desc.value = desc.initializer ? desc.initializer.call(context) : void 0; - desc.initializer = undefined; - } - - if (desc.initializer === void 0) { - Object['define' + 'Property'](target, property, desc); - desc = null; - } - - return desc; -} - -function concatInDict(dict, key, value) { - dict[key] = dict[key] ? dict[key].concat(value) : value; -} - -// Find the relation name before the first dot, and include all other relations after it -// Example: input `animal.kind.breed` output -> `['animal', 'kind.breed']` -var RE_SPLIT_FIRST_RELATION = /([^.]+)\.(.+)/; - -// TODO: find a way to get a list of existing properties automatically. -var FORBIDDEN_ATTRS = ['url', 'urlRoot', 'api', 'isNew', 'isLoading', 'parse', 'save', 'clear']; - -var Model = (_class$1 = (_temp$1 = _class2$1 = function () { - createClass(Model, [{ - key: 'urlRoot', - - // How the model is known at the backend. This is useful when the model is in a relation that has a different name. - value: function urlRoot() { - // Try to auto-generate the URL. - var bname = this.constructor.backendResourceName; - if (bname) { - return '/' + bname + '/'; - } - return null; - } - // Holds original attributes with values, so `clear()` knows what to reset to (quite ugly). - - // Holds activated - nested - relations (e.g. `['animal', 'animal.breed']`) - - // Holds activated - non-nested - relations (e.g. `['animal']`) - - // A `cid` can be used to identify the model locally. - - // URL query params that are added to fetch requests. - - // Holds fields (attrs+relations) that have been changed via setInput() - - - // File state - - }, { - key: 'wrapPendingRequestCount', - value: function wrapPendingRequestCount(promise) { - var _this = this; - - this.__pendingRequestCount++; - - return promise.then(function (res) { - _this.__pendingRequestCount--; - return res; - }).catch(function (err) { - _this.__pendingRequestCount--; - throw err; - }); - } - - // Useful to reference to this model in a relation - that is not yet saved to the backend. - - }, { - key: 'getNegativeId', - value: function getNegativeId() { - return -parseInt(this.cid.replace('m', '')); - } - }, { - key: 'getInternalId', - value: function getInternalId() { - if (this.isNew) { - return this.getNegativeId(); - } - return this[this.constructor.primaryKey]; - } - }, { - key: 'casts', - value: function casts() { - return {}; - } - }, { - key: 'fileFields', - value: function fileFields() { - return this.constructor.fileFields; - } - }, { - key: 'pickFields', - value: function pickFields() { - return this.constructor.pickFields; - } - }, { - key: 'omitFields', - value: function omitFields() { - return this.constructor.omitFields; - } - - // Empty function, but can be overridden if you want to do something after initializing the model. - - }, { - key: 'initialize', - value: function initialize() {} - }, { - key: 'url', - get: function get$$1() { - var id = this[this.constructor.primaryKey]; - return '' + lodash.result(this, 'urlRoot') + (id ? id + '/' : ''); - } - }, { - key: 'isNew', - get: function get$$1() { - return !this[this.constructor.primaryKey]; - } - }, { - key: 'isLoading', - get: function get$$1() { - return this.__pendingRequestCount > 0; - } - }, { - key: 'primaryKey', - set: function set$$1(v) { - invariant(false, '`primaryKey` should be a static property on the model.'); - } - }, { - key: 'backendResourceName', - set: function set$$1(v) { - invariant(false, '`backendResourceName` should be a static property on the model.'); - } - }]); - - function Model(data) { - var _this2 = this; - - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - classCallCheck(this, Model); - this.__attributes = []; - this.__originalAttributes = {}; - this.__activeRelations = []; - this.__activeCurrentRelations = []; - this.api = null; - this.cid = 'm' + lodash.uniqueId(); - - _initDefineProp$1(this, '__backendValidationErrors', _descriptor$1, this); - - _initDefineProp$1(this, '__pendingRequestCount', _descriptor2$1, this); - - _initDefineProp$1(this, '__fetchParams', _descriptor3$1, this); - - _initDefineProp$1(this, '__changes', _descriptor4$1, this); - - _initDefineProp$1(this, '__fileChanges', _descriptor5$1, this); - - _initDefineProp$1(this, '__fileDeletions', _descriptor6, this); - - _initDefineProp$1(this, '__fileExists', _descriptor7, this); - - this.__store = options.store; - this.__repository = options.repository; - // Find all attributes. Not all observables are an attribute. - lodash.forIn(this, function (value, key) { - if (!key.startsWith('__') && mobx.isObservableProp(_this2, key)) { - invariant(!FORBIDDEN_ATTRS.includes(key), 'Forbidden attribute key used: `' + key + '`'); - _this2.__attributes.push(key); - var newValue = value; - // An array or object observable can be mutated, so we want to ensure we always have - // the original not-yet-mutated object/array. - if (mobx.isObservableArray(value)) { - newValue = value.slice(); - } else if (mobx.isObservableObject(value)) { - newValue = Object.assign({}, value); - } - _this2.__originalAttributes[key] = newValue; - } - }); - if (options.relations) { - this.__parseRelations(options.relations); - } - if (data) { - this.parse(data); - } - this.initialize(); - - this.saveFile = this.saveFile.bind(this); - } - - createClass(Model, [{ - key: '__parseRelations', - value: function __parseRelations(activeRelations) { - var _this3 = this; - - this.__activeRelations = activeRelations; - // TODO: No idea why getting the relations only works when it's a Function. - var relations = this.relations && this.relations(); - var relModels = {}; - activeRelations.forEach(function (aRel) { - // If aRel is null, this relation is already defined by another aRel - // IE.: town.restaurants.chef && town - if (aRel === null || !!_this3[aRel]) { - return; - } - var relNames = aRel.match(RE_SPLIT_FIRST_RELATION); - - var currentRel = relNames ? relNames[1] : aRel; - var otherRelNames = relNames && relNames[2]; - var currentProp = relModels[currentRel]; - var otherRels = otherRelNames && [otherRelNames]; - - // When two nested relations are defined next to each other (e.g. `['kind.breed', 'kind.location']`), - // the relation `kind` only needs to be initialized once. - relModels[currentRel] = currentProp ? currentProp.concat(otherRels) : otherRels; - invariant(!_this3.__attributes.includes(currentRel), 'Cannot define `' + currentRel + '` as both an attribute and a relation. You probably need to remove the attribute.'); - if (!_this3.__activeCurrentRelations.includes(currentRel)) { - _this3.__activeCurrentRelations.push(currentRel); - } - }); - // extendObservable where we omit the fields that are already created from other relations - mobx.extendObservable(this, lodash.mapValues(lodash.omit(relModels, Object.keys(relModels).filter(function (rel) { - return !!_this3[rel]; - })), function (otherRelNames, relName) { - var RelModel = relations[relName]; - invariant(RelModel, 'Specified relation "' + relName + '" does not exist on model.'); - var options = { relations: otherRelNames }; - if (RelModel.prototype instanceof Store) { - return new RelModel(options); - } - return new RelModel(null, options); - })); - } - - // Many backends use snake_case for attribute names, so we convert to snake_case by default. - - }, { - key: 'clearUserFieldChanges', - value: function clearUserFieldChanges() { - this.__changes.clear(); - } - }, { - key: 'clearUserFileChanges', - value: function clearUserFileChanges() { - this.__fileChanges = {}; - this.__fileDeletions = {}; - this.__fileExists = {}; - } - }, { - key: 'clearUserChanges', - value: function clearUserChanges() { - this.clearUserFieldChanges(); - this.clearUserFileChanges(); - } - }, { - key: 'toBackend', - value: function toBackend() { - var _this4 = this; - - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var _ref$data = _ref.data, - data = _ref$data === undefined ? {} : _ref$data, - _ref$mapData = _ref.mapData, - mapData = _ref$mapData === undefined ? function (x) { - return x; - } : _ref$mapData, - options = objectWithoutProperties(_ref, ['data', 'mapData']); - - var output = {}; - // By default we'll include all fields (attributes+relations), but sometimes you might want to specify the fields to be included. - var fieldFilter = function fieldFilter(field) { - if (!_this4.fieldFilter(field)) { - return false; - } - if (options.fields) { - return options.fields.includes(field); - } - if (!_this4.isNew && options.onlyChanges) { - var forceFields = options.forceFields || []; - return forceFields.includes(field) || _this4.__changes.includes(field) || _this4[field] instanceof Store && _this4[field].hasSetChanges || - // isNew is always true for relations that haven't been saved. - // If no property has been tweaked, its id serializes as null. - // So, we need to skip saving the id if new and no changes. - _this4[field] instanceof Model && _this4[field].isNew && _this4[field].hasUserChanges; - } - return true; - }; - this.__attributes.filter(fieldFilter).forEach(function (attr) { - if (!attr.startsWith('_')) { - output[_this4.constructor.toBackendAttrKey(attr)] = _this4.__toJSAttr(attr, _this4[attr]); - } - }); - - // Primary key is always forced to be included. - output[this.constructor.primaryKey] = this[this.constructor.primaryKey]; - - // Add active relations as id. - this.__activeCurrentRelations.filter(fieldFilter).forEach(function (currentRel) { - var rel = _this4[currentRel]; - var relBackendName = _this4.constructor.toBackendAttrKey(currentRel); - if (rel instanceof Model) { - output[relBackendName] = rel[rel.constructor.primaryKey]; - } - if (rel instanceof Store) { - output[relBackendName] = rel.mapByPrimaryKey(); - } - }); - - Object.assign(output, data); - return mapData(output); - } - }, { - key: 'toBackendAll', - value: function toBackendAll() { - var _this5 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var nestedRelations = options.nestedRelations || {}; - var data = this.toBackend({ - data: options.data, - mapData: options.mapData, - onlyChanges: options.onlyChanges - }); - - if (data[this.constructor.primaryKey] === null) { - data[this.constructor.primaryKey] = this.getNegativeId(); - } - - var relations = {}; - - this.__activeCurrentRelations.forEach(function (currentRel) { - var rel = _this5[currentRel]; - var relBackendName = _this5.constructor.toBackendAttrKey(currentRel); - var subRelations = nestedRelations[currentRel]; - - if (subRelations !== undefined) { - if (data[relBackendName] === null) { - data[relBackendName] = rel.getNegativeId(); - } else if (lodash.isArray(data[relBackendName])) { - data[relBackendName] = lodash.uniq(data[relBackendName].map(function (pk, i) { - return pk === null ? rel.at(i).getNegativeId() : pk; - })); - } else if (options.onlyChanges && !rel.hasUserChanges) { - return; - } - - var relBackendData = rel.toBackendAll({ - nestedRelations: subRelations, - onlyChanges: options.onlyChanges - }); - - // Sometimes the backend knows the relation by a different name, e.g. the relation is called - // `activities`, but the name in the backend is `activity`. - // In that case, you can add `static backendResourceName = 'activity';` to that model. - var realBackendName = rel.constructor.backendResourceName || relBackendName; - - if (relBackendData.data.length > 0) { - concatInDict(relations, realBackendName, relBackendData.data); - - // De-duplicate relations based on `primaryKey`. - // TODO: Avoid serializing recursively multiple times in the first place? - // TODO: What if different relations have different "freshness"? - relations[realBackendName] = lodash.uniqBy(relations[realBackendName], rel.constructor.primaryKey || rel.Model.primaryKey); - } - - // There could still be changes in nested relations, - // include those anyway! - lodash.forIn(relBackendData.relations, function (relB, key) { - concatInDict(relations, key, relB); - }); - } - }); - - return { data: [data], relations: relations }; - } - - /** - * Makes this model a copy of the specified model - * or returns a copy of the current model when no model to copy is given - * It also clones the changes that were in the specified model. - * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. - * Cloning - * - * @param source {Model} - The model that should be copied - * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} - */ - - }, { - key: 'copy', - value: function copy() { - var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; - - var copiedModel = void 0; - // If our source is not a model it is 'probably' the options - if (source !== undefined && !(source instanceof Model)) { - options = source; - source = undefined; - } - - // Make sure that we have the correct model - if (source === undefined) { - source = this; - copiedModel = new source.constructor({ relations: source.__activeRelations }); - } else if (this.constructor !== source.constructor) { - copiedModel = new source.constructor({ relations: source.__activeRelations }); - } else { - copiedModel = this; - } - - var copyChanges = options.copyChanges; - - // Maintain the relations after copy - // this.__activeRelations = source.__activeRelations; - copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; - - copiedModel.__parseRelations(source.__activeRelations); - // Copy all fields and values from the specified model - copiedModel.parse(source.toJS()); - - // Set only the changed attributes - if (copyChanges) { - copiedModel._copyChanges(source); - } - - return copiedModel; - } - - /** - * Goes over model and all related models to set the changed values and notify the store - * - * @param source - the model to copy - * @param store - the store of the current model, to setChanged if there are changes - * @private - */ - - }, { - key: '_copyChanges', - value: function _copyChanges(source, store) { - var _this6 = this; - - // Maintain the relations after copy - this.__activeRelations = source.__activeRelations; - this.__activeCurrentRelations = source.__activeCurrentRelations; - - // Copy all changed fields and notify the store that there are changes - if (source.__changes.length > 0) { - if (store) { - store.__setChanged = true; - } else if (this.__store) { - this.__store.__setChanged = true; - } - - source.__changes.forEach(function (changedAttribute) { - _this6.setInput(changedAttribute, source[changedAttribute]); - }); - } - // Undefined safety - if (source.__activeCurrentRelations.length > 0) { - // Set the changes for all related models with changes - source.__activeCurrentRelations.forEach(function (relation) { - if (relation && source[relation]) { - if (source[relation].hasUserChanges) { - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); - }); - } - } - }); - } - } - }, { - key: 'toJS', - value: function toJS() { - var _this7 = this; - - var output = {}; - this.__attributes.forEach(function (attr) { - output[attr] = _this7.__toJSAttr(attr, _this7[attr]); - }); - - this.__activeCurrentRelations.forEach(function (currentRel) { - var model = _this7[currentRel]; - if (model) { - output[currentRel] = model.toJS(); - } - }); - return output; - } - }, { - key: '__toJSAttr', - value: function __toJSAttr(attr, value) { - var casts = this.casts(); - var cast = casts[attr]; - if (cast !== undefined) { - return mobx.toJS(cast.toJS(attr, value)); - } - return mobx.toJS(value); - } - }, { - key: 'setFetchParams', - value: function setFetchParams(params) { - this.__fetchParams = Object.assign({}, params); - } - }, { - key: '__parseRepositoryToData', - value: function __parseRepositoryToData(key, repository) { - if (lodash.isArray(key)) { - return lodash.filter(repository, function (m) { - return key.includes(m.id); - }); - } - return lodash.find(repository, { id: key }); - } - }, { - key: '__parseReverseRepositoryToData', - value: function __parseReverseRepositoryToData(reverseKeyName, key, repository) { - var searchKey = {}; - searchKey[reverseKeyName] = key; - return lodash.filter(repository, searchKey); - } - - /** - * We handle the fromBackend recursively. - * But when recursing, we don't send the full repository, we need to only send the repo - * relevant to the relation. - * - * So when we have a customer with a town.restaurants relation, - * we get a "town.restaurants": "restaurant", relMapping from Binder - * - * Here we create a scoped repository. - * The root gets a `town.restaurants` repo, but the `town` relation only gets the `restaurants` repo - */ - - }, { - key: '__scopeBackendResponse', - value: function __scopeBackendResponse(_ref2) { - var _this8 = this; - - var data = _ref2.data, - targetRelName = _ref2.targetRelName, - repos = _ref2.repos, - mapping = _ref2.mapping, - reverseMapping = _ref2.reverseMapping; - - var scopedData = null; - var relevant = false; - var scopedRepos = {}; - var scopedRelMapping = {}; - var scopedReverseRelMapping = {}; - - if (!data) { - return null; - } - - lodash.forIn(mapping, function (repoName, backendRelName) { - var repository = repos[repoName]; - // For backwards compatibility, reverseMapping is optional (for now) - var reverseRelName = reverseMapping ? reverseMapping[backendRelName] : null; - var relName = _this8.constructor.fromBackendAttrKey(backendRelName); - - if (targetRelName === relName) { - var relKey = data[_this8.constructor.toBackendAttrKey(relName)]; - if (relKey !== undefined) { - relevant = true; - scopedData = _this8.__parseRepositoryToData(relKey, repository); - } else if (repository && reverseRelName) { - var pk = data[_this8.constructor.primaryKey]; - relevant = true; - scopedData = _this8.__parseReverseRepositoryToData(reverseRelName, pk, repository); - if (_this8.relations(relName).prototype instanceof Model) { - if (scopedData.length === 0) { - scopedData = null; - } else if (scopedData.length === 1) { - scopedData = scopedData[0]; - } else { - throw new Error('multiple models found for related model'); - } - } - } - return; - } - - if (relName.startsWith(targetRelName + '.')) { - // If we have town.restaurants and the targetRel = town - // we need "restaurants" in the repository - relevant = true; - var backendRelNames = backendRelName.match(RE_SPLIT_FIRST_RELATION); - var scopedBackendRelName = backendRelNames[2]; - scopedRepos[repoName] = repository; - scopedRelMapping[scopedBackendRelName] = repoName; - scopedReverseRelMapping[scopedBackendRelName] = reverseMapping ? reverseMapping[backendRelName] : null; - } - }); - - if (!relevant) { - return null; - } - - return { scopedData: scopedData, scopedRepos: scopedRepos, scopedRelMapping: scopedRelMapping, scopedReverseRelMapping: scopedReverseRelMapping }; - } - - // `data` contains properties for the current model. - // `repos` is an object of "repositories". A repository is - // e.g. "animal_kind", while the relation name would be "kind". - // `relMapping` maps relation names to repositories. - - }, { - key: 'fromBackend', - value: function fromBackend(_ref3) { - var _this9 = this; - - var data = _ref3.data, - repos = _ref3.repos, - relMapping = _ref3.relMapping, - reverseRelMapping = _ref3.reverseRelMapping; - - // We handle the fromBackend recursively. On each relation of the source model - // fromBackend gets called as well, but with data scoped for itself - // - // So when we have a model with a `town.restaurants.chef` relation, - // we call fromBackend on the `town` relation. - lodash.each(this.__activeCurrentRelations, function (relName) { - var rel = _this9[relName]; - var resScoped = _this9.__scopeBackendResponse({ - data: data, - targetRelName: relName, - repos: repos, - mapping: relMapping, - reverseMapping: reverseRelMapping - }); - - // Make sure we don't parse every relation for nothing - if (!resScoped) { - return; - } - - var scopedData = resScoped.scopedData, - scopedRepos = resScoped.scopedRepos, - scopedRelMapping = resScoped.scopedRelMapping, - scopedReverseRelMapping = resScoped.scopedReverseRelMapping; - - rel.fromBackend({ - data: scopedData, - repos: scopedRepos, - relMapping: scopedRelMapping, - reverseRelMapping: scopedReverseRelMapping - }); - }); - - // Now all repositories are set on the relations, start parsing the actual data. - // `parse()` will recursively fill in all relations. - if (data) { - this.parse(data); - } - } - }, { - key: '__getApi', - value: function __getApi() { - invariant(this.api, 'You are trying to perform a API request without an `api` property defined on the model.'); - invariant(lodash.result(this, 'urlRoot'), 'You are trying to perform a API request without an `urlRoot` property defined on the model.'); - return this.api; - } - }, { - key: 'parse', - value: function parse(data) { - var _this10 = this; - - invariant(lodash.isPlainObject(data), 'Parameter supplied to `parse()` is not an object, got: ' + JSON.stringify(data)); - - lodash.forIn(data, function (value, key) { - var attr = _this10.constructor.fromBackendAttrKey(key); - if (_this10.__attributes.includes(attr)) { - _this10[attr] = _this10.__parseAttr(attr, value); - } else if (_this10.__activeCurrentRelations.includes(attr)) { - // In Binder, a relation property is an `int` or `[int]`, referring to its ID. - // However, it can also be an object if there are nested relations (non flattened). - if (lodash.isPlainObject(value) || lodash.isPlainObject(lodash.get(value, '[0]'))) { - _this10[attr].parse(value); - } else if (value === null) { - // The relation is cleared. - _this10[attr].clear(); - } - } - }); - - return this; - } - }, { - key: '__parseAttr', - value: function __parseAttr(attr, value) { - var casts = this.casts(); - var cast = casts[attr]; - if (cast !== undefined) { - return cast.parse(attr, value); - } - return value; - } - }, { - key: 'saveFile', - value: function saveFile(name) { - var _this11 = this; - - var snakeName = camelToSnake(name); - - if (this.__fileChanges[name]) { - var file = this.__fileChanges[name]; - - var data = new FormData(); - data.append(name, file, file.name); - - return this.api.post('' + this.url + snakeName + '/', data, { headers: { 'Content-Type': 'multipart/form-data' } }).then(mobx.action(function (res) { - _this11.__fileExists[name] = true; - delete _this11.__fileChanges[name]; - _this11.saveFromBackend(res); - })); - } else if (this.__fileDeletions[name]) { - if (this.__fileExists[name]) { - return this.api.delete('' + this.url + snakeName + '/').then(mobx.action(function () { - _this11.__fileExists[name] = false; - delete _this11.__fileDeletions[name]; - _this11.saveFromBackend({ data: defineProperty({}, snakeName, null) }); - })); - } else { - delete this.__fileDeletions[name]; - } - } else { - return Promise.resolve(); - } - } - }, { - key: 'saveFiles', - value: function saveFiles() { - return Promise.all(this.fileFields().filter(this.fieldFilter).map(this.saveFile)); - } - - /** - * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation - * errors without actually committing the save - * - * @param options - same as for a normal save request, example: {onlyChanges: true} - */ - - }, { - key: 'validate', - value: function validate() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - // Add the validate parameter - if (options.params) { - options.params.validate = true; - } else { - options.params = { validate: true }; - } - return this.save(options); - } - }, { - key: 'save', - value: function save() { - var _this12 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - this.clearValidationErrors(); - return this.wrapPendingRequestCount(this.__getApi().saveModel({ - url: options.url || this.url, - data: this.toBackend({ - data: options.data, - mapData: options.mapData, - fields: options.fields, - onlyChanges: options.onlyChanges - }), - isNew: this.isNew, - requestOptions: lodash.omit(options, 'url', 'data', 'mapData') - }).then(mobx.action(function (res) { - // Only update the model when we are actually trying to save - if (!options.params || !options.params.validate) { - _this12.saveFromBackend(_extends({}, res, { - data: lodash.omit(res.data, _this12.fileFields().map(camelToSnake)) - })); - _this12.clearUserFieldChanges(); - return _this12.saveFiles().then(function () { - _this12.clearUserFileChanges(); - return Promise.resolve(res); - }); - } - })).catch(mobx.action(function (err) { - if (err.valErrors) { - _this12.parseValidationErrors(err.valErrors); - } - throw err; - }))); - } - }, { - key: 'setInput', - value: function setInput(name, value) { - invariant(this.__attributes.includes(name) || this.__activeCurrentRelations.includes(name), 'Field `' + name + '` does not exist on the model.'); - if (this.fileFields().includes(name)) { - if (this.__fileExists[name] === undefined) { - this.__fileExists[name] = this[name] !== null; - } - if (value) { - this.__fileChanges[name] = value; - delete this.__fileDeletions[name]; - - value = URL.createObjectURL(value) + '?content_type=' + value.type; - } else { - if (!this.__fileChanges[name] || this.__fileChanges[name].existed) { - this.__fileDeletions[name] = true; - } - delete this.__fileChanges[name]; - - value = null; - } - } - if (!this.__changes.includes(name)) { - this.__changes.push(name); - } - if (this.__activeCurrentRelations.includes(name)) { - if (lodash.isArray(value)) { - this[name].clear(); - this[name].add(value.map(function (v) { - return v.toJS(); - })); - } else if (value) { - this[name].parse(value.toJS()); - } else { - this[name].clear(); - } - } else { - this[name] = value; - } - if (this.backendValidationErrors[name]) { - this.__backendValidationErrors = Object.assign(this.backendValidationErrors, defineProperty({}, name, undefined)); - } - } - }, { - key: 'saveAllFiles', - value: function saveAllFiles() { - var relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var promises = [this.saveFiles()]; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = Object.keys(relations)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var rel = _step.value; - - promises.push(this[rel].saveAllFiles(relations[rel])); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - return Promise.all(promises); - } - - /** - * Validates a model and relations by sending a save request to binder with the validate header set. Binder will return the validation - * errors without actually committing the save - * - * @param options - same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} - */ - - }, { - key: 'validateAll', - value: function validateAll() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - // Add the validate option - if (options.params) { - options.params.validate = true; - } else { - options.params = { validate: true }; - } - return this.saveAll(options); - } - }, { - key: 'saveAll', - value: function saveAll() { - var _this13 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - this.clearValidationErrors(); - return this.wrapPendingRequestCount(this.__getApi().saveAllModels({ - url: lodash.result(this, 'urlRoot'), - model: this, - data: this.toBackendAll({ - data: options.data, - mapData: options.mapData, - nestedRelations: relationsToNestedKeys(options.relations || []), - onlyChanges: options.onlyChanges - }), - requestOptions: lodash.omit(options, 'relations', 'data', 'mapData') - }).then(mobx.action(function (res) { - // Only update the models if we are actually trying to save - if (!options.params || !options.params.validate) { - _this13.saveFromBackend(res); - _this13.clearUserFieldChanges(); - - forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); - - return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this13.clearUserFileChanges(); - - forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFileChanges(); - } - }); - - return res; - }); - } - })).catch(mobx.action(function (err) { - if (err.valErrors) { - _this13.parseValidationErrors(err.valErrors); - } - throw err; - }))); - } - - // After saving a model, we should get back an ID mapping from the backend which looks like: - // `{ "animal": [[-1, 10]] }` - - }, { - key: '__parseNewIds', - value: function __parseNewIds(idMaps) { - var _this14 = this; - - var bName = this.constructor.backendResourceName; - if (bName && idMaps[bName]) { - var idMap = idMaps[bName].find(function (ids) { - return ids[0] === _this14.getInternalId(); - }); - if (idMap) { - this[this.constructor.primaryKey] = idMap[1]; - } - } - lodash.each(this.__activeCurrentRelations, function (relName) { - var rel = _this14[relName]; - rel.__parseNewIds(idMaps); - }); - } - }, { - key: 'validationErrorFormatter', - value: function validationErrorFormatter(obj) { - return obj.code; - } - }, { - key: 'parseValidationErrors', - value: function parseValidationErrors(valErrors) { - var _this15 = this; - - var bname = this.constructor.backendResourceName; - - if (valErrors[bname]) { - var id = this.getInternalId(); - // When there is no id or negative id, the backend may use the string 'null'. Bit weird, but eh. - var errorsForModel = valErrors[bname][id] || valErrors[bname]['null']; - if (errorsForModel) { - var camelCasedErrors = lodash.mapKeys(errorsForModel, function (value, key) { - return snakeToCamel(key); - }); - var formattedErrors = lodash.mapValues(camelCasedErrors, function (valError) { - return valError.map(_this15.validationErrorFormatter); - }); - this.__backendValidationErrors = formattedErrors; - } - } - - this.__activeCurrentRelations.forEach(function (currentRel) { - _this15[currentRel].parseValidationErrors(valErrors); - }); - } - }, { - key: 'clearValidationErrors', - value: function clearValidationErrors() { - var _this16 = this; - - this.__backendValidationErrors = {}; - this.__activeCurrentRelations.forEach(function (currentRel) { - _this16[currentRel].clearValidationErrors(); - }); - } - - // This is just a pass-through to make it easier to override parsing backend responses from the backend. - // Sometimes the backend won't return the model after a save because e.g. it is created async. - - }, { - key: 'saveFromBackend', - value: function saveFromBackend(res) { - return this.fromBackend(res); - } - - // TODO: This is a bit hacky... - - }, { - key: 'delete', - value: function _delete() { - var _this17 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var removeFromStore = function removeFromStore() { - return _this17.__store ? _this17.__store.remove(_this17) : null; - }; - if (options.immediate || this.isNew) { - removeFromStore(); - } - if (this.isNew) { - return Promise.resolve(); - } - - return this.wrapPendingRequestCount(this.__getApi().deleteModel({ - url: options.url || this.url, - requestOptions: lodash.omit(options, ['immediate', 'url']) - }).then(mobx.action(function () { - if (!options.immediate) { - removeFromStore(); - } - }))); - } - }, { - key: 'buildFetchData', - value: function buildFetchData(options) { - return Object.assign(this.__getApi().buildFetchModelParams(this), this.__fetchParams, options.data); - } - }, { - key: 'fetch', - value: function fetch() { - var _this18 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - invariant(!this.isNew, 'Trying to fetch model without id!'); - - var data = this.buildFetchData(options); - var promise = this.wrapPendingRequestCount(this.__getApi().fetchModel({ - url: options.url || this.url, - data: data, - requestOptions: lodash.omit(options, ['data', 'url']) - }).then(mobx.action(function (res) { - _this18.fromBackend(res); - }))); - - return promise; - } - }, { - key: 'clear', - value: function clear() { - var _this19 = this; - - lodash.forIn(this.__originalAttributes, function (value, key) { - _this19[key] = value; - }); - - this.__activeCurrentRelations.forEach(function (currentRel) { - _this19[currentRel].clear(); - }); - } - }, { - key: 'hasUserChanges', - get: function get$$1() { - var _this20 = this; - - if (this.__changes.length > 0) { - return true; - } - return this.__activeCurrentRelations.some(function (rel) { - return _this20[rel].hasUserChanges; - }); - } - }, { - key: 'fieldFilter', - get: function get$$1() { - var pickFields = this.pickFields(); - var omitFields = this.omitFields(); - - return function (name) { - return (!pickFields || pickFields.includes(name)) && !omitFields.includes(name); - }; - } - }, { - key: 'backendValidationErrors', - get: function get$$1() { - return this.__backendValidationErrors; - } - }], [{ - key: 'toBackendAttrKey', - value: function toBackendAttrKey(attrKey) { - return camelToSnake(attrKey); - } - - // In the frontend we don't want to deal with those snake_case attr names. - - }, { - key: 'fromBackendAttrKey', - value: function fromBackendAttrKey(attrKey) { - return snakeToCamel(attrKey); - } - }]); - return Model; -}(), _class2$1.primaryKey = 'id', _class2$1.backendResourceName = '', _class2$1.fileFields = [], _class2$1.pickFields = undefined, _class2$1.omitFields = [], _temp$1), (_descriptor$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__backendValidationErrors', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor2$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__pendingRequestCount', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return 0; - } -}), _descriptor3$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fetchParams', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor4$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__changes', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return []; - } -}), _descriptor5$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileChanges', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor6 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileDeletions', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor7 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileExists', [mobx.observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _applyDecoratedDescriptor$1(_class$1.prototype, 'url', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'url'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isNew', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isNew'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isLoading'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '__parseRelations', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '__parseRelations'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'hasUserChanges'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fieldFilter', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fieldFilter'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fromBackend'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parse'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'save', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'save'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'setInput', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'saveAll', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'saveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parseValidationErrors', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parseValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clearValidationErrors', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clearValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'backendValidationErrors', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'backendValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'delete', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'delete'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fetch'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clear'), _class$1.prototype)), _class$1); - -// Function ripped from Django docs. -// See: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax -function csrfSafeMethod(method) { - // These HTTP methods do not require CSRF protection. - return (/^(GET|HEAD|OPTIONS|TRACE)$/i.test(method) - ); -} - -var BinderApi = function () { - function BinderApi() { - classCallCheck(this, BinderApi); - this.baseUrl = null; - this.csrfToken = null; - this.defaultHeaders = {}; - this.axios = axios.create(); - - this.__initializeCsrfHandling(); - } - - createClass(BinderApi, [{ - key: '__initializeCsrfHandling', - value: function __initializeCsrfHandling() { - var _this = this; - - this.axios.interceptors.response.use(null, function (err) { - var status = lodash.get(err, 'response.status'); - var statusErrCode = lodash.get(err, 'response.data.code'); - var doNotRetry = lodash.get(err, 'response.config.doNotRetry'); - if (status === 403 && statusErrCode === 'CSRFFailure' && !doNotRetry) { - return _this.fetchCsrfToken().then(function () { - return _this.axios(_extends({}, err.response.config, { - doNotRetry: true - })); - }); - } - return Promise.reject(err); - }); - } - }, { - key: '__request', - value: function __request(method, url, data, options) { - options || (options = {}); - var useCsrfToken = csrfSafeMethod(method) ? undefined : this.csrfToken; - this.__testUrl(url); - - var axiosOptions = { - method: method, - baseURL: this.baseUrl, - url: url, - data: method !== 'get' && data ? data : undefined, - params: method === 'get' && data ? data : options.params - }; - - Object.assign(axiosOptions, options); - - // Don't clear existing headers when adding `options.headers` - var headers = Object.assign({ - 'Content-Type': 'application/json', - 'X-Csrftoken': useCsrfToken - }, this.defaultHeaders, options.headers); - axiosOptions.headers = headers; - - var xhr = this.axios(axiosOptions); - - // We fork the promise tree as we want to have the error traverse to the listeners - if (this.onRequestError && options.skipRequestError !== true) { - xhr.catch(this.onRequestError); - } - - var onSuccess = options.skipFormatter === true ? Promise.resolve() : this.__responseFormatter; - return xhr.then(onSuccess); - } - }, { - key: 'parseBackendValidationErrors', - value: function parseBackendValidationErrors(response) { - var valErrors = lodash.get(response, 'data.errors'); - if (response.status === 400 && valErrors) { - return valErrors; - } - return null; - } - }, { - key: 'fetchCsrfToken', - value: function fetchCsrfToken() { - var _this2 = this; - - return this.get('/api/bootstrap/').then(function (res) { - _this2.csrfToken = res.csrf_token; - }); - } - }, { - key: '__responseFormatter', - value: function __responseFormatter(res) { - return res.data; - } - }, { - key: '__testUrl', - value: function __testUrl(url) { - if (!url.endsWith('/')) { - throw new Error('Binder does not accept urls that do not have a trailing slash: ' + url); - } - } - }, { - key: 'get', - value: function get$$1(url, data, options) { - return this.__request('get', url, data, options); - } - }, { - key: 'post', - value: function post(url, data, options) { - return this.__request('post', url, data, options); - } - }, { - key: 'patch', - value: function patch(url, data, options) { - return this.__request('patch', url, data, options); - } - }, { - key: 'put', - value: function put(url, data, options) { - return this.__request('put', url, data, options); - } - }, { - key: 'delete', - value: function _delete(url, data, options) { - return this.__request('delete', url, data, options); - } - }, { - key: 'buildFetchModelParams', - value: function buildFetchModelParams(model) { - return { - // TODO: I really dislike that this is comma separated and not an array. - // We should fix this in the Binder API. - with: model.__activeRelations.map(model.constructor.toBackendAttrKey).join(',') || null - }; - } - }, { - key: 'fetchModel', - value: function fetchModel(_ref) { - var url = _ref.url, - data = _ref.data, - requestOptions = _ref.requestOptions; - - return this.get(url, data, requestOptions).then(function (res) { - return { - data: res.data, - repos: res.with, - relMapping: res.with_mapping, - reverseRelMapping: res.with_related_name_mapping - }; - }); - } - }, { - key: 'saveModel', - value: function saveModel(_ref2) { - var _this3 = this; - - var url = _ref2.url, - data = _ref2.data, - isNew = _ref2.isNew, - requestOptions = _ref2.requestOptions; - - var method = isNew ? 'post' : 'patch'; - return this[method](url, data, requestOptions).then(function (newData) { - return { data: newData }; - }).catch(function (err) { - if (err.response) { - err.valErrors = _this3.parseBackendValidationErrors(err.response); - } - throw err; - }); - } - }, { - key: 'saveAllModels', - value: function saveAllModels(_ref3) { - var _this4 = this; - - var url = _ref3.url, - data = _ref3.data, - model = _ref3.model, - requestOptions = _ref3.requestOptions; - - return this.put(url, { - data: data.data, - with: data.relations - }, requestOptions).then(function (res) { - if (res.idmap) { - model.__parseNewIds(res.idmap); - } - return res; - }).catch(function (err) { - if (err.response) { - err.valErrors = _this4.parseBackendValidationErrors(err.response); - } - throw err; - }); - } - }, { - key: 'deleteModel', - value: function deleteModel(_ref4) { - var url = _ref4.url, - requestOptions = _ref4.requestOptions; - - // TODO: kind of silly now, but we'll probably want better error handling soon. - return this.delete(url, null, requestOptions); - } - }, { - key: 'buildFetchStoreParams', - value: function buildFetchStoreParams(store) { - var offset = store.getPageOffset(); - var limit = store.__state.limit; - return { - with: store.__activeRelations.map(store.Model.toBackendAttrKey).join(',') || null, - limit: limit === null ? 'none' : limit, - // Hide offset if zero so the request looks cleaner in DevTools. - offset: offset || null - }; - } - }, { - key: 'fetchStore', - value: function fetchStore(_ref5) { - var url = _ref5.url, - data = _ref5.data, - requestOptions = _ref5.requestOptions; - - return this.get(url, data, requestOptions).then(function (res) { - return { - response: res, - data: res.data, - repos: res.with, - relMapping: res.with_mapping, - reverseRelMapping: res.with_related_name_mapping, - totalRecords: res.meta.total_records - }; - }); - } - }]); - return BinderApi; -}(); - -var DATE_LIB = 'moment'; -var SUPPORTED_DATE_LIBS = ['moment', 'luxon']; - -function configureDateLib(dateLib) { - invariant(SUPPORTED_DATE_LIBS.includes(dateLib), 'Unsupported date lib `' + dateLib + '`. ' + ('(Supported: ' + SUPPORTED_DATE_LIBS.map(function (dateLib) { - return '`' + dateLib + '`'; - }).join(', ') + ')')); - DATE_LIB = dateLib; -} - -function checkMomentInstance(attr, value) { - invariant(moment.isMoment(value), 'Attribute `' + attr + '` is not a moment instance.'); -} - -function checkLuxonDateTime(attr, value) { - invariant(moment.isMoment(value), 'Attribute `' + attr + '` is not a luxon DateTime.'); -} - -var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; -var LUXON_DATETIME_FORMAT = 'yyy-LL-ddTHH:mm:ssZZZ'; - -var CASTS = { - momentDate: { - parse: function parse(attr, value) { - if (value === null || value === undefined) { - return null; - } - return moment(value, 'YYYY-MM-DD'); - }, - toJS: function toJS(attr, value) { - if (value === null || value === undefined) { - return null; - } - checkMomentInstance(attr, value); - return value.format('YYYY-MM-DD'); - }, - - dateLib: 'moment' - }, - momentDatetime: { - parse: function parse(attr, value) { - if (value === null) { - return null; - } - return moment(value); - }, - toJS: function toJS(attr, value) { - if (value === null) { - return null; - } - checkMomentInstance(attr, value); - return value.toJSON(); // Use ISO8601 notation, adjusted to UTC - }, - - dateLib: 'moment' - }, - luxonDate: { - parse: function parse(attr, value) { - if (value === null || value === undefined) { - return null; - } - return luxon.DateTime.fromFormat(value, LUXON_DATE_FORMAT); - }, - toJS: function toJS(attr, value) { - if (value === null || value === undefined) { - return null; - } - checkLuxonDateTime(attr, value); - return value.toFormat(LUXON_DATE_FORMAT); - }, - - dateLib: 'luxon' - }, - luxonDatetime: { - parse: function parse(attr, value) { - if (value === null) { - return null; - } - return luxon.DateTime.fromFormat(value, LUXON_DATETIME_FORMAT); - }, - toJS: function toJS(attr, value) { - if (value === null) { - return null; - } - checkLuxonDateTime(attr, value); - return value.toFormat(LUXON_DATETIME_FORMAT); - }, - - dateLib: 'luxon' - }, - date: { - parse: function parse() { - var _CASTS$; - - return (_CASTS$ = CASTS[DATE_LIB + 'Date']).parse.apply(_CASTS$, arguments); - }, - toJS: function toJS() { - var _CASTS$2; - - return (_CASTS$2 = CASTS[DATE_LIB + 'Date']).toJS.apply(_CASTS$2, arguments); - }, - - get dateLib() { - return DATE_LIB; - } - }, - datetime: { - parse: function parse() { - var _CASTS$3; - - return (_CASTS$3 = CASTS[DATE_LIB + 'Datetime']).parse.apply(_CASTS$3, arguments); - }, - toJS: function toJS() { - var _CASTS$4; - - return (_CASTS$4 = CASTS[DATE_LIB + 'Datetime']).toJS.apply(_CASTS$4, arguments); - }, - - get dateLib() { - return DATE_LIB; - } - }, - enum: function _enum(expectedValues) { - invariant(lodash.isArray(expectedValues), 'Invalid argument suplied to `Casts.enum`, expected an instance of array.'); - function checkExpectedValues(attr, value) { - if (value === null) { - return null; - } - if (expectedValues.includes(value)) { - return value; - } - invariant(false, 'Value set to attribute `' + attr + '`, ' + JSON.stringify(value) + ', is not one of the allowed enum: ' + JSON.stringify(expectedValues)); - } - return { - parse: checkExpectedValues, - toJS: checkExpectedValues - }; - } -}; - -exports.Model = Model; -exports.Store = Store; -exports.BinderApi = BinderApi; -exports.Casts = CASTS; -exports.configureDateLib = configureDateLib; diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js deleted file mode 100644 index 15ba60c..0000000 --- a/dist/mobx-spine.es.js +++ /dev/null @@ -1,2324 +0,0 @@ -import { observable, computed, action, autorun, isObservableProp, extendObservable, isObservableArray, isObservableObject, toJS } from 'mobx'; -import { isArray, map, filter, find, sortBy, forIn, omit, isPlainObject, result, uniqBy, each, mapValues, get, uniqueId, uniq, mapKeys } from 'lodash'; -import axios from 'axios'; -import moment from 'moment'; -import { DateTime } from 'luxon'; - -function invariant(condition) { - var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Illegal state'; - - if (!condition) { - throw new Error('[mobx-spine] ' + message); - } -} - -// lodash's `snakeCase` method removes dots from the string; this breaks mobx-spine -function camelToSnake(s) { - return s.replace(/([A-Z])/g, function ($1) { - return '_' + $1.toLowerCase(); - }); -} - -// lodash's `camelCase` method removes dots from the string; this breaks mobx-spine -function snakeToCamel(s) { - if (s.startsWith('_')) { - return s; - } - return s.replace(/_\w/g, function (m) { - return m[1].toUpperCase(); - }); -} - -// ['kind.breed', 'owner'] => { 'owner': {}, 'kind': {'breed': {}}} -function relationsToNestedKeys(relations) { - var nestedRelations = {}; - - relations.forEach(function (rel) { - var current = nestedRelations; - var components = rel.split('.'); - var len = components.length; - - for (var i = 0; i < len; ++i) { - var head = components[i]; - if (current[head] === undefined) { - current[head] = {}; - } - current = current[head]; - } - }); - - return nestedRelations; -} - -// Use output of relationsToNestedKeys to iterate each relation, fn is called on each model and store. -function forNestedRelations(model, nestedRelations, fn) { - Object.keys(nestedRelations).forEach(function (key) { - if (Object.keys(nestedRelations[key]).length > 0) { - if (model[key].forEach) { - model[key].forEach(function (m) { - forNestedRelations(m, nestedRelations[key], fn); - }); - - fn(model); - } else { - forNestedRelations(model[key], nestedRelations[key], fn); - } - } - - if (model[key].forEach) { - model[key].forEach(fn); - } - - fn(model[key]); - }); -} - -var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -}; - -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; - }; -}(); - -var defineProperty = function (obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - - return obj; -}; - -var _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; -}; - -var objectWithoutProperties = function (obj, keys) { - var target = {}; - - for (var i in obj) { - if (keys.indexOf(i) >= 0) continue; - if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; - target[i] = obj[i]; - } - - return target; -}; - -var _class, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _class2, _temp; - -function _initDefineProp(target, property, descriptor, context) { - if (!descriptor) return; - Object.defineProperty(target, property, { - enumerable: descriptor.enumerable, - configurable: descriptor.configurable, - writable: descriptor.writable, - value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 - }); -} - -function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { - var desc = {}; - Object['ke' + 'ys'](descriptor).forEach(function (key) { - desc[key] = descriptor[key]; - }); - desc.enumerable = !!desc.enumerable; - desc.configurable = !!desc.configurable; - - if ('value' in desc || desc.initializer) { - desc.writable = true; - } - - desc = decorators.slice().reverse().reduce(function (desc, decorator) { - return decorator(target, property, desc) || desc; - }, desc); - - if (context && desc.initializer !== void 0) { - desc.value = desc.initializer ? desc.initializer.call(context) : void 0; - desc.initializer = undefined; - } - - if (desc.initializer === void 0) { - Object['define' + 'Property'](target, property, desc); - desc = null; - } - - return desc; -} -var AVAILABLE_CONST_OPTIONS = ['relations', 'limit', 'comparator', 'params', 'repository']; - -var Store = (_class = (_temp = _class2 = function () { - createClass(Store, [{ - key: 'url', - value: function url() { - // Try to auto-generate the URL. - var bname = this.constructor.backendResourceName; - if (bname) { - return '/' + bname + '/'; - } - return null; - } - // The set of models has changed - - // Holds the fetch parameters - - }, { - key: 'initialize', - - - // Empty function, but can be overridden if you want to do something after initializing the model. - value: function initialize() {} - }, { - key: 'isLoading', - get: function get$$1() { - return this.__pendingRequestCount > 0; - } - }, { - key: 'length', - get: function get$$1() { - return this.models.length; - } - }, { - key: 'backendResourceName', - set: function set$$1(v) { - invariant(false, '`backendResourceName` should be a static property on the store.'); - } - }]); - - function Store() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - classCallCheck(this, Store); - - _initDefineProp(this, 'models', _descriptor, this); - - _initDefineProp(this, 'params', _descriptor2, this); - - _initDefineProp(this, '__pendingRequestCount', _descriptor3, this); - - _initDefineProp(this, '__setChanged', _descriptor4, this); - - _initDefineProp(this, '__state', _descriptor5, this); - - this.__activeRelations = []; - this.Model = null; - this.api = null; - - invariant(isPlainObject(options), 'Store only accepts an object with options. Chain `.parse(data)` to add models.'); - forIn(options, function (value, option) { - invariant(AVAILABLE_CONST_OPTIONS.includes(option), 'Unknown option passed to store: ' + option); - }); - this.__repository = options.repository; - if (options.relations) { - this.__parseRelations(options.relations); - } - if (options.limit !== undefined) { - this.setLimit(options.limit); - } - if (options.comparator) { - this.comparator = options.comparator; - } - if (options.params) { - this.params = options.params; - } - this.initialize(); - } - - createClass(Store, [{ - key: '__parseRelations', - value: function __parseRelations(activeRelations) { - this.__activeRelations = activeRelations; - } - }, { - key: '__getApi', - value: function __getApi() { - invariant(this.api, 'You are trying to perform a API request without an `api` property defined on the store.'); - invariant(result(this, 'url'), 'You are trying to perform a API request without an `url` property defined on the store.'); - return this.api; - } - }, { - key: 'fromBackend', - value: function fromBackend(_ref) { - var _this = this; - - var data = _ref.data, - repos = _ref.repos, - relMapping = _ref.relMapping, - reverseRelMapping = _ref.reverseRelMapping; - - invariant(data, 'Backend error. Data is not set. HINT: DID YOU FORGET THE M2M again?'); - - this.models.replace(data.map(function (record) { - // TODO: I'm not happy at all about how this looks. - // We'll need to finetune some things, but hey, for now it works. - var model = _this._newModel(); - model.fromBackend({ - data: record, - repos: repos, - relMapping: relMapping, - reverseRelMapping: reverseRelMapping - }); - return model; - })); - this.sort(); - } - }, { - key: '_newModel', - value: function _newModel() { - var model = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - - return new this.Model(model, { - store: this, - relations: this.__activeRelations - }); - } - }, { - key: 'sort', - value: function sort() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - invariant(isPlainObject(options), 'Expecting a plain object for options.'); - if (!this.comparator) { - return this; - } - if (typeof this.comparator === 'string') { - this.models.replace(this.sortBy(this.comparator)); - } else { - this.models.replace(this.models.slice().sort(this.comparator)); - } - return this; - } - }, { - key: 'parse', - value: function parse(models) { - invariant(isArray(models), 'Parameter supplied to `parse()` is not an array, got: ' + JSON.stringify(models)); - // Parse does not mutate __setChanged, as it is used in - // fromBackend in the model... - this.models.replace(models.map(this._newModel.bind(this))); - this.sort(); - - return this; - } - }, { - key: 'parseValidationErrors', - value: function parseValidationErrors(valErrors) { - this.each(function (model) { - model.parseValidationErrors(valErrors); - }); - } - }, { - key: 'clearValidationErrors', - value: function clearValidationErrors() { - this.each(function (model) { - model.clearValidationErrors(); - }); - } - }, { - key: 'add', - value: function add(models) { - var _this2 = this; - - var singular = !isArray(models); - models = singular ? [models] : models.slice(); - - var modelInstances = models.map(this._newModel.bind(this)); - - modelInstances.forEach(function (modelInstance) { - var primaryValue = modelInstance[_this2.Model.primaryKey]; - invariant(!primaryValue || !_this2.get(primaryValue), 'A model with the same primary key value "' + primaryValue + '" already exists in this store.'); - _this2.__setChanged = true; - _this2.models.push(modelInstance); - }); - this.sort(); - - return singular ? modelInstances[0] : modelInstances; - } - }, { - key: 'remove', - value: function remove(models) { - var _this3 = this; - - var singular = !isArray(models); - models = singular ? [models] : models.slice(); - - models.forEach(function (model) { - return _this3.models.remove(model); - }); - if (models.length > 0) { - this.__setChanged = true; - } - return models; - } - }, { - key: 'removeById', - value: function removeById(ids) { - var _this4 = this; - - var singular = !isArray(ids); - ids = singular ? [ids] : ids.slice(); - invariant(!ids.some(isNaN), 'Cannot remove a model by id that is not a number: ' + JSON.stringify(ids)); - - var models = ids.map(function (id) { - return _this4.get(id); - }); - - models.forEach(function (model) { - if (model) { - _this4.models.remove(model); - _this4.__setChanged = true; - } - }); - - return models; - } - }, { - key: 'clear', - value: function clear() { - var length = this.models.length; - this.models.clear(); - - if (length > 0) { - this.__setChanged = true; - } - } - }, { - key: 'buildFetchData', - value: function buildFetchData(options) { - return Object.assign(this.__getApi().buildFetchStoreParams(this), this.params, options.data); - } - }, { - key: 'fetch', - value: function fetch() { - var _this5 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - - var data = this.buildFetchData(options); - var promise = this.wrapPendingRequestCount(this.__getApi().fetchStore({ - url: options.url || result(this, 'url'), - data: data, - requestOptions: omit(options, 'data') - }).then(action(function (res) { - _this5.__state.totalRecords = res.totalRecords; - _this5.fromBackend(res); - - return res.response; - }))); - - return promise; - } - }, { - key: '__parseNewIds', - value: function __parseNewIds(idMaps) { - this.each(function (model) { - return model.__parseNewIds(idMaps); - }); - } - }, { - key: 'toJS', - value: function toJS$$1() { - return this.models.map(function (model) { - return model.toJS(); - }); - } - - // Methods for pagination. - - }, { - key: 'getPageOffset', - value: function getPageOffset() { - return (this.__state.currentPage - 1) * this.__state.limit; - } - }, { - key: 'setLimit', - value: function setLimit(limit) { - invariant(!limit || Number.isInteger(limit), 'Page limit should be a number or falsy value.'); - this.__state.limit = limit || null; - } - }, { - key: 'getNextPage', - value: function getNextPage() { - invariant(this.hasNextPage, 'There is no next page.'); - this.__state.currentPage += 1; - return this.fetch(); - } - }, { - key: 'getPreviousPage', - value: function getPreviousPage() { - invariant(this.hasPreviousPage, 'There is no previous page.'); - this.__state.currentPage -= 1; - return this.fetch(); - } - }, { - key: 'setPage', - value: function setPage() { - var page = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - invariant(Number.isInteger(page) && page >= 1, 'Page should be a number above 1.'); - this.__state.currentPage = page; - if (options.fetch === undefined || options.fetch) { - return this.fetch(); - } - invariant( - // Always allow to go to page 1. - page <= (this.totalPages || 1), 'Page should be between 1 and ' + this.totalPages + '.'); - return Promise.resolve(); - } - }, { - key: 'clearSetChanges', - value: function clearSetChanges() { - this.__setChanged = false; - } - }, { - key: 'toBackendAll', - value: function toBackendAll() { - var _this6 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var relevantModels = options.onlyChanges ? this.models.filter(function (model) { - return model.isNew || model.hasUserChanges; - }) : this.models; - var modelData = relevantModels.map(function (model) { - return model.toBackendAll(options); - }); - - var data = []; - var relations = {}; - - modelData.forEach(function (model) { - data = data.concat(model.data); - forIn(model.relations, function (relModel, key) { - relations[key] = relations[key] ? relations[key].concat(relModel) : relModel; - // TODO: this primaryKey is not the primaryKey of the relation we're de-duplicating... - relations[key] = uniqBy(relations[key], _this6.Model.primaryKey); - }); - }); - - return { data: data, relations: relations }; - } - - // Create a new instance of this store with a predicate applied. - // This new store will be automatically kept in-sync with all models that adhere to the predicate. - - }, { - key: 'virtualStore', - value: function virtualStore(_ref2) { - var _this7 = this; - - var filter$$1 = _ref2.filter, - comparator = _ref2.comparator; - - var store = new this.constructor({ - relations: this.__activeRelations, - comparator: comparator - }); - - // Oh gawd MobX is so awesome. - var events = autorun(function () { - var models = _this7.filter(filter$$1); - store.models.replace(models); - store.sort(); - - // When the parent store is busy, make sure the virtual store is - // also busy. - store.__pendingRequestCount = _this7.__pendingRequestCount; - }); - - store.unsubscribeVirtualStore = events; - - return store; - } - - // Helper methods to read models. - - }, { - key: 'get', - value: function get$$1(id) { - // The id can be defined as a string or int, but we want it to work in both cases. - return this.models.find(function (model) { - return model[model.constructor.primaryKey] == id; - } // eslint-disable-line eqeqeq - ); - } - }, { - key: 'getByIds', - value: function getByIds(ids) { - return this.models.filter(function (model) { - var id = model[model.constructor.primaryKey]; - return ids.includes(id) || ids.includes('' + id); - }); - } - }, { - key: 'map', - value: function map$$1(predicate) { - return map(this.models, predicate); - } - }, { - key: 'mapByPrimaryKey', - value: function mapByPrimaryKey() { - return this.map(this.Model.primaryKey); - } - }, { - key: 'filter', - value: function filter$$1(predicate) { - return filter(this.models, predicate); - } - }, { - key: 'find', - value: function find$$1(predicate) { - return find(this.models, predicate); - } - }, { - key: 'each', - value: function each$$1(predicate) { - return this.models.forEach(predicate); - } - }, { - key: 'forEach', - value: function forEach(predicate) { - return this.models.forEach(predicate); - } - }, { - key: 'sortBy', - value: function sortBy$$1(iteratees) { - return sortBy(this.models, iteratees); - } - }, { - key: 'at', - value: function at(index) { - var zeroLength = this.length - 1; - invariant(index <= zeroLength, 'Index ' + index + ' is out of bounds (max ' + zeroLength + ').'); - if (index < 0) { - index += this.length; - } - return this.models[index]; - } - }, { - key: 'wrapPendingRequestCount', - value: function wrapPendingRequestCount(promise) { - var _this8 = this; - - this.__pendingRequestCount++; - - return promise.then(function (res) { - _this8.__pendingRequestCount--; - return res; - }).catch(function (err) { - _this8.__pendingRequestCount--; - throw err; - }); - } - }, { - key: 'saveAllFiles', - value: function saveAllFiles() { - var relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var promises = []; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = this.models[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var model = _step.value; - - promises.push(model.saveAllFiles(relations)); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - return Promise.all(promises); - } - }, { - key: 'totalPages', - get: function get$$1() { - if (!this.__state.limit) { - return 0; - } - return Math.ceil(this.__state.totalRecords / this.__state.limit); - } - }, { - key: 'currentPage', - get: function get$$1() { - return this.__state.currentPage; - } - }, { - key: 'hasNextPage', - get: function get$$1() { - return this.__state.currentPage + 1 <= this.totalPages; - } - }, { - key: 'hasPreviousPage', - get: function get$$1() { - return this.__state.currentPage > 1; - } - }, { - key: 'hasUserChanges', - get: function get$$1() { - return this.hasSetChanges || this.models.some(function (m) { - return m.hasUserChanges; - }); - } - - // TODO: Maybe we can keep track of what got added and what got - // removed exactly. For now this should be enough. - - }, { - key: 'hasSetChanges', - get: function get$$1() { - return this.__setChanged; - } - }]); - return Store; -}(), _class2.backendResourceName = '', _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, 'models', [observable], { - enumerable: true, - initializer: function initializer() { - return []; - } -}), _descriptor2 = _applyDecoratedDescriptor(_class.prototype, 'params', [observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor3 = _applyDecoratedDescriptor(_class.prototype, '__pendingRequestCount', [observable], { - enumerable: true, - initializer: function initializer() { - return 0; - } -}), _descriptor4 = _applyDecoratedDescriptor(_class.prototype, '__setChanged', [observable], { - enumerable: true, - initializer: function initializer() { - return false; - } -}), _descriptor5 = _applyDecoratedDescriptor(_class.prototype, '__state', [observable], { - enumerable: true, - initializer: function initializer() { - return { - currentPage: 1, - limit: 25, - totalRecords: 0 - }; - } -}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype)), _class); - -var _class$1, _descriptor$1, _descriptor2$1, _descriptor3$1, _descriptor4$1, _descriptor5$1, _descriptor6, _descriptor7, _class2$1, _temp$1; - -function _initDefineProp$1(target, property, descriptor, context) { - if (!descriptor) return; - Object.defineProperty(target, property, { - enumerable: descriptor.enumerable, - configurable: descriptor.configurable, - writable: descriptor.writable, - value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 - }); -} - -function _applyDecoratedDescriptor$1(target, property, decorators, descriptor, context) { - var desc = {}; - Object['ke' + 'ys'](descriptor).forEach(function (key) { - desc[key] = descriptor[key]; - }); - desc.enumerable = !!desc.enumerable; - desc.configurable = !!desc.configurable; - - if ('value' in desc || desc.initializer) { - desc.writable = true; - } - - desc = decorators.slice().reverse().reduce(function (desc, decorator) { - return decorator(target, property, desc) || desc; - }, desc); - - if (context && desc.initializer !== void 0) { - desc.value = desc.initializer ? desc.initializer.call(context) : void 0; - desc.initializer = undefined; - } - - if (desc.initializer === void 0) { - Object['define' + 'Property'](target, property, desc); - desc = null; - } - - return desc; -} - -function concatInDict(dict, key, value) { - dict[key] = dict[key] ? dict[key].concat(value) : value; -} - -// Find the relation name before the first dot, and include all other relations after it -// Example: input `animal.kind.breed` output -> `['animal', 'kind.breed']` -var RE_SPLIT_FIRST_RELATION = /([^.]+)\.(.+)/; - -// TODO: find a way to get a list of existing properties automatically. -var FORBIDDEN_ATTRS = ['url', 'urlRoot', 'api', 'isNew', 'isLoading', 'parse', 'save', 'clear']; - -var Model = (_class$1 = (_temp$1 = _class2$1 = function () { - createClass(Model, [{ - key: 'urlRoot', - - // How the model is known at the backend. This is useful when the model is in a relation that has a different name. - value: function urlRoot() { - // Try to auto-generate the URL. - var bname = this.constructor.backendResourceName; - if (bname) { - return '/' + bname + '/'; - } - return null; - } - // Holds original attributes with values, so `clear()` knows what to reset to (quite ugly). - - // Holds activated - nested - relations (e.g. `['animal', 'animal.breed']`) - - // Holds activated - non-nested - relations (e.g. `['animal']`) - - // A `cid` can be used to identify the model locally. - - // URL query params that are added to fetch requests. - - // Holds fields (attrs+relations) that have been changed via setInput() - - - // File state - - }, { - key: 'wrapPendingRequestCount', - value: function wrapPendingRequestCount(promise) { - var _this = this; - - this.__pendingRequestCount++; - - return promise.then(function (res) { - _this.__pendingRequestCount--; - return res; - }).catch(function (err) { - _this.__pendingRequestCount--; - throw err; - }); - } - - // Useful to reference to this model in a relation - that is not yet saved to the backend. - - }, { - key: 'getNegativeId', - value: function getNegativeId() { - return -parseInt(this.cid.replace('m', '')); - } - }, { - key: 'getInternalId', - value: function getInternalId() { - if (this.isNew) { - return this.getNegativeId(); - } - return this[this.constructor.primaryKey]; - } - }, { - key: 'casts', - value: function casts() { - return {}; - } - }, { - key: 'fileFields', - value: function fileFields() { - return this.constructor.fileFields; - } - }, { - key: 'pickFields', - value: function pickFields() { - return this.constructor.pickFields; - } - }, { - key: 'omitFields', - value: function omitFields() { - return this.constructor.omitFields; - } - - // Empty function, but can be overridden if you want to do something after initializing the model. - - }, { - key: 'initialize', - value: function initialize() {} - }, { - key: 'url', - get: function get$$1() { - var id = this[this.constructor.primaryKey]; - return '' + result(this, 'urlRoot') + (id ? id + '/' : ''); - } - }, { - key: 'isNew', - get: function get$$1() { - return !this[this.constructor.primaryKey]; - } - }, { - key: 'isLoading', - get: function get$$1() { - return this.__pendingRequestCount > 0; - } - }, { - key: 'primaryKey', - set: function set$$1(v) { - invariant(false, '`primaryKey` should be a static property on the model.'); - } - }, { - key: 'backendResourceName', - set: function set$$1(v) { - invariant(false, '`backendResourceName` should be a static property on the model.'); - } - }]); - - function Model(data) { - var _this2 = this; - - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - classCallCheck(this, Model); - this.__attributes = []; - this.__originalAttributes = {}; - this.__activeRelations = []; - this.__activeCurrentRelations = []; - this.api = null; - this.cid = 'm' + uniqueId(); - - _initDefineProp$1(this, '__backendValidationErrors', _descriptor$1, this); - - _initDefineProp$1(this, '__pendingRequestCount', _descriptor2$1, this); - - _initDefineProp$1(this, '__fetchParams', _descriptor3$1, this); - - _initDefineProp$1(this, '__changes', _descriptor4$1, this); - - _initDefineProp$1(this, '__fileChanges', _descriptor5$1, this); - - _initDefineProp$1(this, '__fileDeletions', _descriptor6, this); - - _initDefineProp$1(this, '__fileExists', _descriptor7, this); - - this.__store = options.store; - this.__repository = options.repository; - // Find all attributes. Not all observables are an attribute. - forIn(this, function (value, key) { - if (!key.startsWith('__') && isObservableProp(_this2, key)) { - invariant(!FORBIDDEN_ATTRS.includes(key), 'Forbidden attribute key used: `' + key + '`'); - _this2.__attributes.push(key); - var newValue = value; - // An array or object observable can be mutated, so we want to ensure we always have - // the original not-yet-mutated object/array. - if (isObservableArray(value)) { - newValue = value.slice(); - } else if (isObservableObject(value)) { - newValue = Object.assign({}, value); - } - _this2.__originalAttributes[key] = newValue; - } - }); - if (options.relations) { - this.__parseRelations(options.relations); - } - if (data) { - this.parse(data); - } - this.initialize(); - - this.saveFile = this.saveFile.bind(this); - } - - createClass(Model, [{ - key: '__parseRelations', - value: function __parseRelations(activeRelations) { - var _this3 = this; - - this.__activeRelations = activeRelations; - // TODO: No idea why getting the relations only works when it's a Function. - var relations = this.relations && this.relations(); - var relModels = {}; - activeRelations.forEach(function (aRel) { - // If aRel is null, this relation is already defined by another aRel - // IE.: town.restaurants.chef && town - if (aRel === null || !!_this3[aRel]) { - return; - } - var relNames = aRel.match(RE_SPLIT_FIRST_RELATION); - - var currentRel = relNames ? relNames[1] : aRel; - var otherRelNames = relNames && relNames[2]; - var currentProp = relModels[currentRel]; - var otherRels = otherRelNames && [otherRelNames]; - - // When two nested relations are defined next to each other (e.g. `['kind.breed', 'kind.location']`), - // the relation `kind` only needs to be initialized once. - relModels[currentRel] = currentProp ? currentProp.concat(otherRels) : otherRels; - invariant(!_this3.__attributes.includes(currentRel), 'Cannot define `' + currentRel + '` as both an attribute and a relation. You probably need to remove the attribute.'); - if (!_this3.__activeCurrentRelations.includes(currentRel)) { - _this3.__activeCurrentRelations.push(currentRel); - } - }); - // extendObservable where we omit the fields that are already created from other relations - extendObservable(this, mapValues(omit(relModels, Object.keys(relModels).filter(function (rel) { - return !!_this3[rel]; - })), function (otherRelNames, relName) { - var RelModel = relations[relName]; - invariant(RelModel, 'Specified relation "' + relName + '" does not exist on model.'); - var options = { relations: otherRelNames }; - if (RelModel.prototype instanceof Store) { - return new RelModel(options); - } - return new RelModel(null, options); - })); - } - - // Many backends use snake_case for attribute names, so we convert to snake_case by default. - - }, { - key: 'clearUserFieldChanges', - value: function clearUserFieldChanges() { - this.__changes.clear(); - } - }, { - key: 'clearUserFileChanges', - value: function clearUserFileChanges() { - this.__fileChanges = {}; - this.__fileDeletions = {}; - this.__fileExists = {}; - } - }, { - key: 'clearUserChanges', - value: function clearUserChanges() { - this.clearUserFieldChanges(); - this.clearUserFileChanges(); - } - }, { - key: 'toBackend', - value: function toBackend() { - var _this4 = this; - - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var _ref$data = _ref.data, - data = _ref$data === undefined ? {} : _ref$data, - _ref$mapData = _ref.mapData, - mapData = _ref$mapData === undefined ? function (x) { - return x; - } : _ref$mapData, - options = objectWithoutProperties(_ref, ['data', 'mapData']); - - var output = {}; - // By default we'll include all fields (attributes+relations), but sometimes you might want to specify the fields to be included. - var fieldFilter = function fieldFilter(field) { - if (!_this4.fieldFilter(field)) { - return false; - } - if (options.fields) { - return options.fields.includes(field); - } - if (!_this4.isNew && options.onlyChanges) { - var forceFields = options.forceFields || []; - return forceFields.includes(field) || _this4.__changes.includes(field) || _this4[field] instanceof Store && _this4[field].hasSetChanges || - // isNew is always true for relations that haven't been saved. - // If no property has been tweaked, its id serializes as null. - // So, we need to skip saving the id if new and no changes. - _this4[field] instanceof Model && _this4[field].isNew && _this4[field].hasUserChanges; - } - return true; - }; - this.__attributes.filter(fieldFilter).forEach(function (attr) { - if (!attr.startsWith('_')) { - output[_this4.constructor.toBackendAttrKey(attr)] = _this4.__toJSAttr(attr, _this4[attr]); - } - }); - - // Primary key is always forced to be included. - output[this.constructor.primaryKey] = this[this.constructor.primaryKey]; - - // Add active relations as id. - this.__activeCurrentRelations.filter(fieldFilter).forEach(function (currentRel) { - var rel = _this4[currentRel]; - var relBackendName = _this4.constructor.toBackendAttrKey(currentRel); - if (rel instanceof Model) { - output[relBackendName] = rel[rel.constructor.primaryKey]; - } - if (rel instanceof Store) { - output[relBackendName] = rel.mapByPrimaryKey(); - } - }); - - Object.assign(output, data); - return mapData(output); - } - }, { - key: 'toBackendAll', - value: function toBackendAll() { - var _this5 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var nestedRelations = options.nestedRelations || {}; - var data = this.toBackend({ - data: options.data, - mapData: options.mapData, - onlyChanges: options.onlyChanges - }); - - if (data[this.constructor.primaryKey] === null) { - data[this.constructor.primaryKey] = this.getNegativeId(); - } - - var relations = {}; - - this.__activeCurrentRelations.forEach(function (currentRel) { - var rel = _this5[currentRel]; - var relBackendName = _this5.constructor.toBackendAttrKey(currentRel); - var subRelations = nestedRelations[currentRel]; - - if (subRelations !== undefined) { - if (data[relBackendName] === null) { - data[relBackendName] = rel.getNegativeId(); - } else if (isArray(data[relBackendName])) { - data[relBackendName] = uniq(data[relBackendName].map(function (pk, i) { - return pk === null ? rel.at(i).getNegativeId() : pk; - })); - } else if (options.onlyChanges && !rel.hasUserChanges) { - return; - } - - var relBackendData = rel.toBackendAll({ - nestedRelations: subRelations, - onlyChanges: options.onlyChanges - }); - - // Sometimes the backend knows the relation by a different name, e.g. the relation is called - // `activities`, but the name in the backend is `activity`. - // In that case, you can add `static backendResourceName = 'activity';` to that model. - var realBackendName = rel.constructor.backendResourceName || relBackendName; - - if (relBackendData.data.length > 0) { - concatInDict(relations, realBackendName, relBackendData.data); - - // De-duplicate relations based on `primaryKey`. - // TODO: Avoid serializing recursively multiple times in the first place? - // TODO: What if different relations have different "freshness"? - relations[realBackendName] = uniqBy(relations[realBackendName], rel.constructor.primaryKey || rel.Model.primaryKey); - } - - // There could still be changes in nested relations, - // include those anyway! - forIn(relBackendData.relations, function (relB, key) { - concatInDict(relations, key, relB); - }); - } - }); - - return { data: [data], relations: relations }; - } - - /** - * Makes this model a copy of the specified model - * or returns a copy of the current model when no model to copy is given - * It also clones the changes that were in the specified model. - * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. - * Cloning - * - * @param source {Model} - The model that should be copied - * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} - */ - - }, { - key: 'copy', - value: function copy() { - var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; - - var copiedModel = void 0; - // If our source is not a model it is 'probably' the options - if (source !== undefined && !(source instanceof Model)) { - options = source; - source = undefined; - } - - // Make sure that we have the correct model - if (source === undefined) { - source = this; - copiedModel = new source.constructor({ relations: source.__activeRelations }); - } else if (this.constructor !== source.constructor) { - copiedModel = new source.constructor({ relations: source.__activeRelations }); - } else { - copiedModel = this; - } - - var copyChanges = options.copyChanges; - - // Maintain the relations after copy - // this.__activeRelations = source.__activeRelations; - copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; - - copiedModel.__parseRelations(source.__activeRelations); - // Copy all fields and values from the specified model - copiedModel.parse(source.toJS()); - - // Set only the changed attributes - if (copyChanges) { - copiedModel._copyChanges(source); - } - - return copiedModel; - } - - /** - * Goes over model and all related models to set the changed values and notify the store - * - * @param source - the model to copy - * @param store - the store of the current model, to setChanged if there are changes - * @private - */ - - }, { - key: '_copyChanges', - value: function _copyChanges(source, store) { - var _this6 = this; - - // Maintain the relations after copy - this.__activeRelations = source.__activeRelations; - this.__activeCurrentRelations = source.__activeCurrentRelations; - - // Copy all changed fields and notify the store that there are changes - if (source.__changes.length > 0) { - if (store) { - store.__setChanged = true; - } else if (this.__store) { - this.__store.__setChanged = true; - } - - source.__changes.forEach(function (changedAttribute) { - _this6.setInput(changedAttribute, source[changedAttribute]); - }); - } - // Undefined safety - if (source.__activeCurrentRelations.length > 0) { - // Set the changes for all related models with changes - source.__activeCurrentRelations.forEach(function (relation) { - if (relation && source[relation]) { - if (source[relation].hasUserChanges) { - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); - }); - } - } - }); - } - } - }, { - key: 'toJS', - value: function toJS$$1() { - var _this7 = this; - - var output = {}; - this.__attributes.forEach(function (attr) { - output[attr] = _this7.__toJSAttr(attr, _this7[attr]); - }); - - this.__activeCurrentRelations.forEach(function (currentRel) { - var model = _this7[currentRel]; - if (model) { - output[currentRel] = model.toJS(); - } - }); - return output; - } - }, { - key: '__toJSAttr', - value: function __toJSAttr(attr, value) { - var casts = this.casts(); - var cast = casts[attr]; - if (cast !== undefined) { - return toJS(cast.toJS(attr, value)); - } - return toJS(value); - } - }, { - key: 'setFetchParams', - value: function setFetchParams(params) { - this.__fetchParams = Object.assign({}, params); - } - }, { - key: '__parseRepositoryToData', - value: function __parseRepositoryToData(key, repository) { - if (isArray(key)) { - return filter(repository, function (m) { - return key.includes(m.id); - }); - } - return find(repository, { id: key }); - } - }, { - key: '__parseReverseRepositoryToData', - value: function __parseReverseRepositoryToData(reverseKeyName, key, repository) { - var searchKey = {}; - searchKey[reverseKeyName] = key; - return filter(repository, searchKey); - } - - /** - * We handle the fromBackend recursively. - * But when recursing, we don't send the full repository, we need to only send the repo - * relevant to the relation. - * - * So when we have a customer with a town.restaurants relation, - * we get a "town.restaurants": "restaurant", relMapping from Binder - * - * Here we create a scoped repository. - * The root gets a `town.restaurants` repo, but the `town` relation only gets the `restaurants` repo - */ - - }, { - key: '__scopeBackendResponse', - value: function __scopeBackendResponse(_ref2) { - var _this8 = this; - - var data = _ref2.data, - targetRelName = _ref2.targetRelName, - repos = _ref2.repos, - mapping = _ref2.mapping, - reverseMapping = _ref2.reverseMapping; - - var scopedData = null; - var relevant = false; - var scopedRepos = {}; - var scopedRelMapping = {}; - var scopedReverseRelMapping = {}; - - if (!data) { - return null; - } - - forIn(mapping, function (repoName, backendRelName) { - var repository = repos[repoName]; - // For backwards compatibility, reverseMapping is optional (for now) - var reverseRelName = reverseMapping ? reverseMapping[backendRelName] : null; - var relName = _this8.constructor.fromBackendAttrKey(backendRelName); - - if (targetRelName === relName) { - var relKey = data[_this8.constructor.toBackendAttrKey(relName)]; - if (relKey !== undefined) { - relevant = true; - scopedData = _this8.__parseRepositoryToData(relKey, repository); - } else if (repository && reverseRelName) { - var pk = data[_this8.constructor.primaryKey]; - relevant = true; - scopedData = _this8.__parseReverseRepositoryToData(reverseRelName, pk, repository); - if (_this8.relations(relName).prototype instanceof Model) { - if (scopedData.length === 0) { - scopedData = null; - } else if (scopedData.length === 1) { - scopedData = scopedData[0]; - } else { - throw new Error('multiple models found for related model'); - } - } - } - return; - } - - if (relName.startsWith(targetRelName + '.')) { - // If we have town.restaurants and the targetRel = town - // we need "restaurants" in the repository - relevant = true; - var backendRelNames = backendRelName.match(RE_SPLIT_FIRST_RELATION); - var scopedBackendRelName = backendRelNames[2]; - scopedRepos[repoName] = repository; - scopedRelMapping[scopedBackendRelName] = repoName; - scopedReverseRelMapping[scopedBackendRelName] = reverseMapping ? reverseMapping[backendRelName] : null; - } - }); - - if (!relevant) { - return null; - } - - return { scopedData: scopedData, scopedRepos: scopedRepos, scopedRelMapping: scopedRelMapping, scopedReverseRelMapping: scopedReverseRelMapping }; - } - - // `data` contains properties for the current model. - // `repos` is an object of "repositories". A repository is - // e.g. "animal_kind", while the relation name would be "kind". - // `relMapping` maps relation names to repositories. - - }, { - key: 'fromBackend', - value: function fromBackend(_ref3) { - var _this9 = this; - - var data = _ref3.data, - repos = _ref3.repos, - relMapping = _ref3.relMapping, - reverseRelMapping = _ref3.reverseRelMapping; - - // We handle the fromBackend recursively. On each relation of the source model - // fromBackend gets called as well, but with data scoped for itself - // - // So when we have a model with a `town.restaurants.chef` relation, - // we call fromBackend on the `town` relation. - each(this.__activeCurrentRelations, function (relName) { - var rel = _this9[relName]; - var resScoped = _this9.__scopeBackendResponse({ - data: data, - targetRelName: relName, - repos: repos, - mapping: relMapping, - reverseMapping: reverseRelMapping - }); - - // Make sure we don't parse every relation for nothing - if (!resScoped) { - return; - } - - var scopedData = resScoped.scopedData, - scopedRepos = resScoped.scopedRepos, - scopedRelMapping = resScoped.scopedRelMapping, - scopedReverseRelMapping = resScoped.scopedReverseRelMapping; - - rel.fromBackend({ - data: scopedData, - repos: scopedRepos, - relMapping: scopedRelMapping, - reverseRelMapping: scopedReverseRelMapping - }); - }); - - // Now all repositories are set on the relations, start parsing the actual data. - // `parse()` will recursively fill in all relations. - if (data) { - this.parse(data); - } - } - }, { - key: '__getApi', - value: function __getApi() { - invariant(this.api, 'You are trying to perform a API request without an `api` property defined on the model.'); - invariant(result(this, 'urlRoot'), 'You are trying to perform a API request without an `urlRoot` property defined on the model.'); - return this.api; - } - }, { - key: 'parse', - value: function parse(data) { - var _this10 = this; - - invariant(isPlainObject(data), 'Parameter supplied to `parse()` is not an object, got: ' + JSON.stringify(data)); - - forIn(data, function (value, key) { - var attr = _this10.constructor.fromBackendAttrKey(key); - if (_this10.__attributes.includes(attr)) { - _this10[attr] = _this10.__parseAttr(attr, value); - } else if (_this10.__activeCurrentRelations.includes(attr)) { - // In Binder, a relation property is an `int` or `[int]`, referring to its ID. - // However, it can also be an object if there are nested relations (non flattened). - if (isPlainObject(value) || isPlainObject(get(value, '[0]'))) { - _this10[attr].parse(value); - } else if (value === null) { - // The relation is cleared. - _this10[attr].clear(); - } - } - }); - - return this; - } - }, { - key: '__parseAttr', - value: function __parseAttr(attr, value) { - var casts = this.casts(); - var cast = casts[attr]; - if (cast !== undefined) { - return cast.parse(attr, value); - } - return value; - } - }, { - key: 'saveFile', - value: function saveFile(name) { - var _this11 = this; - - var snakeName = camelToSnake(name); - - if (this.__fileChanges[name]) { - var file = this.__fileChanges[name]; - - var data = new FormData(); - data.append(name, file, file.name); - - return this.api.post('' + this.url + snakeName + '/', data, { headers: { 'Content-Type': 'multipart/form-data' } }).then(action(function (res) { - _this11.__fileExists[name] = true; - delete _this11.__fileChanges[name]; - _this11.saveFromBackend(res); - })); - } else if (this.__fileDeletions[name]) { - if (this.__fileExists[name]) { - return this.api.delete('' + this.url + snakeName + '/').then(action(function () { - _this11.__fileExists[name] = false; - delete _this11.__fileDeletions[name]; - _this11.saveFromBackend({ data: defineProperty({}, snakeName, null) }); - })); - } else { - delete this.__fileDeletions[name]; - } - } else { - return Promise.resolve(); - } - } - }, { - key: 'saveFiles', - value: function saveFiles() { - return Promise.all(this.fileFields().filter(this.fieldFilter).map(this.saveFile)); - } - - /** - * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation - * errors without actually committing the save - * - * @param options - same as for a normal save request, example: {onlyChanges: true} - */ - - }, { - key: 'validate', - value: function validate() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - // Add the validate parameter - if (options.params) { - options.params.validate = true; - } else { - options.params = { validate: true }; - } - return this.save(options); - } - }, { - key: 'save', - value: function save() { - var _this12 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - this.clearValidationErrors(); - return this.wrapPendingRequestCount(this.__getApi().saveModel({ - url: options.url || this.url, - data: this.toBackend({ - data: options.data, - mapData: options.mapData, - fields: options.fields, - onlyChanges: options.onlyChanges - }), - isNew: this.isNew, - requestOptions: omit(options, 'url', 'data', 'mapData') - }).then(action(function (res) { - // Only update the model when we are actually trying to save - if (!options.params || !options.params.validate) { - _this12.saveFromBackend(_extends({}, res, { - data: omit(res.data, _this12.fileFields().map(camelToSnake)) - })); - _this12.clearUserFieldChanges(); - return _this12.saveFiles().then(function () { - _this12.clearUserFileChanges(); - return Promise.resolve(res); - }); - } - })).catch(action(function (err) { - if (err.valErrors) { - _this12.parseValidationErrors(err.valErrors); - } - throw err; - }))); - } - }, { - key: 'setInput', - value: function setInput(name, value) { - invariant(this.__attributes.includes(name) || this.__activeCurrentRelations.includes(name), 'Field `' + name + '` does not exist on the model.'); - if (this.fileFields().includes(name)) { - if (this.__fileExists[name] === undefined) { - this.__fileExists[name] = this[name] !== null; - } - if (value) { - this.__fileChanges[name] = value; - delete this.__fileDeletions[name]; - - value = URL.createObjectURL(value) + '?content_type=' + value.type; - } else { - if (!this.__fileChanges[name] || this.__fileChanges[name].existed) { - this.__fileDeletions[name] = true; - } - delete this.__fileChanges[name]; - - value = null; - } - } - if (!this.__changes.includes(name)) { - this.__changes.push(name); - } - if (this.__activeCurrentRelations.includes(name)) { - if (isArray(value)) { - this[name].clear(); - this[name].add(value.map(function (v) { - return v.toJS(); - })); - } else if (value) { - this[name].parse(value.toJS()); - } else { - this[name].clear(); - } - } else { - this[name] = value; - } - if (this.backendValidationErrors[name]) { - this.__backendValidationErrors = Object.assign(this.backendValidationErrors, defineProperty({}, name, undefined)); - } - } - }, { - key: 'saveAllFiles', - value: function saveAllFiles() { - var relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var promises = [this.saveFiles()]; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = Object.keys(relations)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var rel = _step.value; - - promises.push(this[rel].saveAllFiles(relations[rel])); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - return Promise.all(promises); - } - - /** - * Validates a model and relations by sending a save request to binder with the validate header set. Binder will return the validation - * errors without actually committing the save - * - * @param options - same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} - */ - - }, { - key: 'validateAll', - value: function validateAll() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - // Add the validate option - if (options.params) { - options.params.validate = true; - } else { - options.params = { validate: true }; - } - return this.saveAll(options); - } - }, { - key: 'saveAll', - value: function saveAll() { - var _this13 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - this.clearValidationErrors(); - return this.wrapPendingRequestCount(this.__getApi().saveAllModels({ - url: result(this, 'urlRoot'), - model: this, - data: this.toBackendAll({ - data: options.data, - mapData: options.mapData, - nestedRelations: relationsToNestedKeys(options.relations || []), - onlyChanges: options.onlyChanges - }), - requestOptions: omit(options, 'relations', 'data', 'mapData') - }).then(action(function (res) { - // Only update the models if we are actually trying to save - if (!options.params || !options.params.validate) { - _this13.saveFromBackend(res); - _this13.clearUserFieldChanges(); - - forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); - - return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this13.clearUserFileChanges(); - - forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFileChanges(); - } - }); - - return res; - }); - } - })).catch(action(function (err) { - if (err.valErrors) { - _this13.parseValidationErrors(err.valErrors); - } - throw err; - }))); - } - - // After saving a model, we should get back an ID mapping from the backend which looks like: - // `{ "animal": [[-1, 10]] }` - - }, { - key: '__parseNewIds', - value: function __parseNewIds(idMaps) { - var _this14 = this; - - var bName = this.constructor.backendResourceName; - if (bName && idMaps[bName]) { - var idMap = idMaps[bName].find(function (ids) { - return ids[0] === _this14.getInternalId(); - }); - if (idMap) { - this[this.constructor.primaryKey] = idMap[1]; - } - } - each(this.__activeCurrentRelations, function (relName) { - var rel = _this14[relName]; - rel.__parseNewIds(idMaps); - }); - } - }, { - key: 'validationErrorFormatter', - value: function validationErrorFormatter(obj) { - return obj.code; - } - }, { - key: 'parseValidationErrors', - value: function parseValidationErrors(valErrors) { - var _this15 = this; - - var bname = this.constructor.backendResourceName; - - if (valErrors[bname]) { - var id = this.getInternalId(); - // When there is no id or negative id, the backend may use the string 'null'. Bit weird, but eh. - var errorsForModel = valErrors[bname][id] || valErrors[bname]['null']; - if (errorsForModel) { - var camelCasedErrors = mapKeys(errorsForModel, function (value, key) { - return snakeToCamel(key); - }); - var formattedErrors = mapValues(camelCasedErrors, function (valError) { - return valError.map(_this15.validationErrorFormatter); - }); - this.__backendValidationErrors = formattedErrors; - } - } - - this.__activeCurrentRelations.forEach(function (currentRel) { - _this15[currentRel].parseValidationErrors(valErrors); - }); - } - }, { - key: 'clearValidationErrors', - value: function clearValidationErrors() { - var _this16 = this; - - this.__backendValidationErrors = {}; - this.__activeCurrentRelations.forEach(function (currentRel) { - _this16[currentRel].clearValidationErrors(); - }); - } - - // This is just a pass-through to make it easier to override parsing backend responses from the backend. - // Sometimes the backend won't return the model after a save because e.g. it is created async. - - }, { - key: 'saveFromBackend', - value: function saveFromBackend(res) { - return this.fromBackend(res); - } - - // TODO: This is a bit hacky... - - }, { - key: 'delete', - value: function _delete() { - var _this17 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var removeFromStore = function removeFromStore() { - return _this17.__store ? _this17.__store.remove(_this17) : null; - }; - if (options.immediate || this.isNew) { - removeFromStore(); - } - if (this.isNew) { - return Promise.resolve(); - } - - return this.wrapPendingRequestCount(this.__getApi().deleteModel({ - url: options.url || this.url, - requestOptions: omit(options, ['immediate', 'url']) - }).then(action(function () { - if (!options.immediate) { - removeFromStore(); - } - }))); - } - }, { - key: 'buildFetchData', - value: function buildFetchData(options) { - return Object.assign(this.__getApi().buildFetchModelParams(this), this.__fetchParams, options.data); - } - }, { - key: 'fetch', - value: function fetch() { - var _this18 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - invariant(!this.isNew, 'Trying to fetch model without id!'); - - var data = this.buildFetchData(options); - var promise = this.wrapPendingRequestCount(this.__getApi().fetchModel({ - url: options.url || this.url, - data: data, - requestOptions: omit(options, ['data', 'url']) - }).then(action(function (res) { - _this18.fromBackend(res); - }))); - - return promise; - } - }, { - key: 'clear', - value: function clear() { - var _this19 = this; - - forIn(this.__originalAttributes, function (value, key) { - _this19[key] = value; - }); - - this.__activeCurrentRelations.forEach(function (currentRel) { - _this19[currentRel].clear(); - }); - } - }, { - key: 'hasUserChanges', - get: function get$$1() { - var _this20 = this; - - if (this.__changes.length > 0) { - return true; - } - return this.__activeCurrentRelations.some(function (rel) { - return _this20[rel].hasUserChanges; - }); - } - }, { - key: 'fieldFilter', - get: function get$$1() { - var pickFields = this.pickFields(); - var omitFields = this.omitFields(); - - return function (name) { - return (!pickFields || pickFields.includes(name)) && !omitFields.includes(name); - }; - } - }, { - key: 'backendValidationErrors', - get: function get$$1() { - return this.__backendValidationErrors; - } - }], [{ - key: 'toBackendAttrKey', - value: function toBackendAttrKey(attrKey) { - return camelToSnake(attrKey); - } - - // In the frontend we don't want to deal with those snake_case attr names. - - }, { - key: 'fromBackendAttrKey', - value: function fromBackendAttrKey(attrKey) { - return snakeToCamel(attrKey); - } - }]); - return Model; -}(), _class2$1.primaryKey = 'id', _class2$1.backendResourceName = '', _class2$1.fileFields = [], _class2$1.pickFields = undefined, _class2$1.omitFields = [], _temp$1), (_descriptor$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__backendValidationErrors', [observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor2$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__pendingRequestCount', [observable], { - enumerable: true, - initializer: function initializer() { - return 0; - } -}), _descriptor3$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fetchParams', [observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor4$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__changes', [observable], { - enumerable: true, - initializer: function initializer() { - return []; - } -}), _descriptor5$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileChanges', [observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor6 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileDeletions', [observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _descriptor7 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileExists', [observable], { - enumerable: true, - initializer: function initializer() { - return {}; - } -}), _applyDecoratedDescriptor$1(_class$1.prototype, 'url', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'url'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isNew', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isNew'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isLoading'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '__parseRelations', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '__parseRelations'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'hasUserChanges'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fieldFilter', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fieldFilter'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fromBackend'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parse'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'save', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'save'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'setInput', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'saveAll', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'saveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parseValidationErrors', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parseValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clearValidationErrors', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clearValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'backendValidationErrors', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'backendValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'delete', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'delete'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fetch'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clear'), _class$1.prototype)), _class$1); - -// Function ripped from Django docs. -// See: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax -function csrfSafeMethod(method) { - // These HTTP methods do not require CSRF protection. - return (/^(GET|HEAD|OPTIONS|TRACE)$/i.test(method) - ); -} - -var BinderApi = function () { - function BinderApi() { - classCallCheck(this, BinderApi); - this.baseUrl = null; - this.csrfToken = null; - this.defaultHeaders = {}; - this.axios = axios.create(); - - this.__initializeCsrfHandling(); - } - - createClass(BinderApi, [{ - key: '__initializeCsrfHandling', - value: function __initializeCsrfHandling() { - var _this = this; - - this.axios.interceptors.response.use(null, function (err) { - var status = get(err, 'response.status'); - var statusErrCode = get(err, 'response.data.code'); - var doNotRetry = get(err, 'response.config.doNotRetry'); - if (status === 403 && statusErrCode === 'CSRFFailure' && !doNotRetry) { - return _this.fetchCsrfToken().then(function () { - return _this.axios(_extends({}, err.response.config, { - doNotRetry: true - })); - }); - } - return Promise.reject(err); - }); - } - }, { - key: '__request', - value: function __request(method, url, data, options) { - options || (options = {}); - var useCsrfToken = csrfSafeMethod(method) ? undefined : this.csrfToken; - this.__testUrl(url); - - var axiosOptions = { - method: method, - baseURL: this.baseUrl, - url: url, - data: method !== 'get' && data ? data : undefined, - params: method === 'get' && data ? data : options.params - }; - - Object.assign(axiosOptions, options); - - // Don't clear existing headers when adding `options.headers` - var headers = Object.assign({ - 'Content-Type': 'application/json', - 'X-Csrftoken': useCsrfToken - }, this.defaultHeaders, options.headers); - axiosOptions.headers = headers; - - var xhr = this.axios(axiosOptions); - - // We fork the promise tree as we want to have the error traverse to the listeners - if (this.onRequestError && options.skipRequestError !== true) { - xhr.catch(this.onRequestError); - } - - var onSuccess = options.skipFormatter === true ? Promise.resolve() : this.__responseFormatter; - return xhr.then(onSuccess); - } - }, { - key: 'parseBackendValidationErrors', - value: function parseBackendValidationErrors(response) { - var valErrors = get(response, 'data.errors'); - if (response.status === 400 && valErrors) { - return valErrors; - } - return null; - } - }, { - key: 'fetchCsrfToken', - value: function fetchCsrfToken() { - var _this2 = this; - - return this.get('/api/bootstrap/').then(function (res) { - _this2.csrfToken = res.csrf_token; - }); - } - }, { - key: '__responseFormatter', - value: function __responseFormatter(res) { - return res.data; - } - }, { - key: '__testUrl', - value: function __testUrl(url) { - if (!url.endsWith('/')) { - throw new Error('Binder does not accept urls that do not have a trailing slash: ' + url); - } - } - }, { - key: 'get', - value: function get$$1(url, data, options) { - return this.__request('get', url, data, options); - } - }, { - key: 'post', - value: function post(url, data, options) { - return this.__request('post', url, data, options); - } - }, { - key: 'patch', - value: function patch(url, data, options) { - return this.__request('patch', url, data, options); - } - }, { - key: 'put', - value: function put(url, data, options) { - return this.__request('put', url, data, options); - } - }, { - key: 'delete', - value: function _delete(url, data, options) { - return this.__request('delete', url, data, options); - } - }, { - key: 'buildFetchModelParams', - value: function buildFetchModelParams(model) { - return { - // TODO: I really dislike that this is comma separated and not an array. - // We should fix this in the Binder API. - with: model.__activeRelations.map(model.constructor.toBackendAttrKey).join(',') || null - }; - } - }, { - key: 'fetchModel', - value: function fetchModel(_ref) { - var url = _ref.url, - data = _ref.data, - requestOptions = _ref.requestOptions; - - return this.get(url, data, requestOptions).then(function (res) { - return { - data: res.data, - repos: res.with, - relMapping: res.with_mapping, - reverseRelMapping: res.with_related_name_mapping - }; - }); - } - }, { - key: 'saveModel', - value: function saveModel(_ref2) { - var _this3 = this; - - var url = _ref2.url, - data = _ref2.data, - isNew = _ref2.isNew, - requestOptions = _ref2.requestOptions; - - var method = isNew ? 'post' : 'patch'; - return this[method](url, data, requestOptions).then(function (newData) { - return { data: newData }; - }).catch(function (err) { - if (err.response) { - err.valErrors = _this3.parseBackendValidationErrors(err.response); - } - throw err; - }); - } - }, { - key: 'saveAllModels', - value: function saveAllModels(_ref3) { - var _this4 = this; - - var url = _ref3.url, - data = _ref3.data, - model = _ref3.model, - requestOptions = _ref3.requestOptions; - - return this.put(url, { - data: data.data, - with: data.relations - }, requestOptions).then(function (res) { - if (res.idmap) { - model.__parseNewIds(res.idmap); - } - return res; - }).catch(function (err) { - if (err.response) { - err.valErrors = _this4.parseBackendValidationErrors(err.response); - } - throw err; - }); - } - }, { - key: 'deleteModel', - value: function deleteModel(_ref4) { - var url = _ref4.url, - requestOptions = _ref4.requestOptions; - - // TODO: kind of silly now, but we'll probably want better error handling soon. - return this.delete(url, null, requestOptions); - } - }, { - key: 'buildFetchStoreParams', - value: function buildFetchStoreParams(store) { - var offset = store.getPageOffset(); - var limit = store.__state.limit; - return { - with: store.__activeRelations.map(store.Model.toBackendAttrKey).join(',') || null, - limit: limit === null ? 'none' : limit, - // Hide offset if zero so the request looks cleaner in DevTools. - offset: offset || null - }; - } - }, { - key: 'fetchStore', - value: function fetchStore(_ref5) { - var url = _ref5.url, - data = _ref5.data, - requestOptions = _ref5.requestOptions; - - return this.get(url, data, requestOptions).then(function (res) { - return { - response: res, - data: res.data, - repos: res.with, - relMapping: res.with_mapping, - reverseRelMapping: res.with_related_name_mapping, - totalRecords: res.meta.total_records - }; - }); - } - }]); - return BinderApi; -}(); - -var DATE_LIB = 'moment'; -var SUPPORTED_DATE_LIBS = ['moment', 'luxon']; - -function configureDateLib(dateLib) { - invariant(SUPPORTED_DATE_LIBS.includes(dateLib), 'Unsupported date lib `' + dateLib + '`. ' + ('(Supported: ' + SUPPORTED_DATE_LIBS.map(function (dateLib) { - return '`' + dateLib + '`'; - }).join(', ') + ')')); - DATE_LIB = dateLib; -} - -function checkMomentInstance(attr, value) { - invariant(moment.isMoment(value), 'Attribute `' + attr + '` is not a moment instance.'); -} - -function checkLuxonDateTime(attr, value) { - invariant(moment.isMoment(value), 'Attribute `' + attr + '` is not a luxon DateTime.'); -} - -var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; -var LUXON_DATETIME_FORMAT = 'yyy-LL-ddTHH:mm:ssZZZ'; - -var CASTS = { - momentDate: { - parse: function parse(attr, value) { - if (value === null || value === undefined) { - return null; - } - return moment(value, 'YYYY-MM-DD'); - }, - toJS: function toJS$$1(attr, value) { - if (value === null || value === undefined) { - return null; - } - checkMomentInstance(attr, value); - return value.format('YYYY-MM-DD'); - }, - - dateLib: 'moment' - }, - momentDatetime: { - parse: function parse(attr, value) { - if (value === null) { - return null; - } - return moment(value); - }, - toJS: function toJS$$1(attr, value) { - if (value === null) { - return null; - } - checkMomentInstance(attr, value); - return value.toJSON(); // Use ISO8601 notation, adjusted to UTC - }, - - dateLib: 'moment' - }, - luxonDate: { - parse: function parse(attr, value) { - if (value === null || value === undefined) { - return null; - } - return DateTime.fromFormat(value, LUXON_DATE_FORMAT); - }, - toJS: function toJS$$1(attr, value) { - if (value === null || value === undefined) { - return null; - } - checkLuxonDateTime(attr, value); - return value.toFormat(LUXON_DATE_FORMAT); - }, - - dateLib: 'luxon' - }, - luxonDatetime: { - parse: function parse(attr, value) { - if (value === null) { - return null; - } - return DateTime.fromFormat(value, LUXON_DATETIME_FORMAT); - }, - toJS: function toJS$$1(attr, value) { - if (value === null) { - return null; - } - checkLuxonDateTime(attr, value); - return value.toFormat(LUXON_DATETIME_FORMAT); - }, - - dateLib: 'luxon' - }, - date: { - parse: function parse() { - var _CASTS$; - - return (_CASTS$ = CASTS[DATE_LIB + 'Date']).parse.apply(_CASTS$, arguments); - }, - toJS: function toJS$$1() { - var _CASTS$2; - - return (_CASTS$2 = CASTS[DATE_LIB + 'Date']).toJS.apply(_CASTS$2, arguments); - }, - - get dateLib() { - return DATE_LIB; - } - }, - datetime: { - parse: function parse() { - var _CASTS$3; - - return (_CASTS$3 = CASTS[DATE_LIB + 'Datetime']).parse.apply(_CASTS$3, arguments); - }, - toJS: function toJS$$1() { - var _CASTS$4; - - return (_CASTS$4 = CASTS[DATE_LIB + 'Datetime']).toJS.apply(_CASTS$4, arguments); - }, - - get dateLib() { - return DATE_LIB; - } - }, - enum: function _enum(expectedValues) { - invariant(isArray(expectedValues), 'Invalid argument suplied to `Casts.enum`, expected an instance of array.'); - function checkExpectedValues(attr, value) { - if (value === null) { - return null; - } - if (expectedValues.includes(value)) { - return value; - } - invariant(false, 'Value set to attribute `' + attr + '`, ' + JSON.stringify(value) + ', is not one of the allowed enum: ' + JSON.stringify(expectedValues)); - } - return { - parse: checkExpectedValues, - toJS: checkExpectedValues - }; - } -}; - -export { Model, Store, BinderApi, CASTS as Casts, configureDateLib }; diff --git a/src/Model.js b/src/Model.js index b387780..87ce470 100644 --- a/src/Model.js +++ b/src/Model.js @@ -490,10 +490,15 @@ export default class Model { source.__activeCurrentRelations.forEach((relation) => { if (relation && source[relation]) { if (source[relation].hasUserChanges) { - // Set the changes for all related models with changes - source[relation].models.forEach((relatedModel, index) => { - this[relation].models[index]._copyChanges(relatedModel, this[relation]); - }); + if (source[relation].models) { // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach((relatedModel, index) => { + this[relation].models[index]._copyChanges(relatedModel, this[relation]); + }); + } else { + // Set the changes for the related model + this[relation].__copyChanges(source[relation], undefined) + } } } }); From 8fd4c839161a9618149afaa6addaa2c02355ec0a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 13:54:14 +0200 Subject: [PATCH 19/45] revert file delete --- dist/mobx-spine.cjs.js | 2340 ++++++++++++++++++++++++++++++++++++++++ dist/mobx-spine.es.js | 2330 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 4670 insertions(+) create mode 100644 dist/mobx-spine.cjs.js create mode 100644 dist/mobx-spine.es.js diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js new file mode 100644 index 0000000..678a10d --- /dev/null +++ b/dist/mobx-spine.cjs.js @@ -0,0 +1,2340 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var mobx = require('mobx'); +var lodash = require('lodash'); +var axios = _interopDefault(require('axios')); +var moment = _interopDefault(require('moment')); +var luxon = require('luxon'); + +function invariant(condition) { + var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Illegal state'; + + if (!condition) { + throw new Error('[mobx-spine] ' + message); + } +} + +// lodash's `snakeCase` method removes dots from the string; this breaks mobx-spine +function camelToSnake(s) { + return s.replace(/([A-Z])/g, function ($1) { + return '_' + $1.toLowerCase(); + }); +} + +// lodash's `camelCase` method removes dots from the string; this breaks mobx-spine +function snakeToCamel(s) { + if (s.startsWith('_')) { + return s; + } + return s.replace(/_\w/g, function (m) { + return m[1].toUpperCase(); + }); +} + +// ['kind.breed', 'owner'] => { 'owner': {}, 'kind': {'breed': {}}} +function relationsToNestedKeys(relations) { + var nestedRelations = {}; + + relations.forEach(function (rel) { + var current = nestedRelations; + var components = rel.split('.'); + var len = components.length; + + for (var i = 0; i < len; ++i) { + var head = components[i]; + if (current[head] === undefined) { + current[head] = {}; + } + current = current[head]; + } + }); + + return nestedRelations; +} + +// Use output of relationsToNestedKeys to iterate each relation, fn is called on each model and store. +function forNestedRelations(model, nestedRelations, fn) { + Object.keys(nestedRelations).forEach(function (key) { + if (Object.keys(nestedRelations[key]).length > 0) { + if (model[key].forEach) { + model[key].forEach(function (m) { + forNestedRelations(m, nestedRelations[key], fn); + }); + + fn(model); + } else { + forNestedRelations(model[key], nestedRelations[key], fn); + } + } + + if (model[key].forEach) { + model[key].forEach(fn); + } + + fn(model[key]); + }); +} + +var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +}; + +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; + }; +}(); + +var defineProperty = function (obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; +}; + +var _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; +}; + +var objectWithoutProperties = function (obj, keys) { + var target = {}; + + for (var i in obj) { + if (keys.indexOf(i) >= 0) continue; + if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; + target[i] = obj[i]; + } + + return target; +}; + +var _class, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _class2, _temp; + +function _initDefineProp(target, property, descriptor, context) { + if (!descriptor) return; + Object.defineProperty(target, property, { + enumerable: descriptor.enumerable, + configurable: descriptor.configurable, + writable: descriptor.writable, + value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 + }); +} + +function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { + var desc = {}; + Object['ke' + 'ys'](descriptor).forEach(function (key) { + desc[key] = descriptor[key]; + }); + desc.enumerable = !!desc.enumerable; + desc.configurable = !!desc.configurable; + + if ('value' in desc || desc.initializer) { + desc.writable = true; + } + + desc = decorators.slice().reverse().reduce(function (desc, decorator) { + return decorator(target, property, desc) || desc; + }, desc); + + if (context && desc.initializer !== void 0) { + desc.value = desc.initializer ? desc.initializer.call(context) : void 0; + desc.initializer = undefined; + } + + if (desc.initializer === void 0) { + Object['define' + 'Property'](target, property, desc); + desc = null; + } + + return desc; +} +var AVAILABLE_CONST_OPTIONS = ['relations', 'limit', 'comparator', 'params', 'repository']; + +var Store = (_class = (_temp = _class2 = function () { + createClass(Store, [{ + key: 'url', + value: function url() { + // Try to auto-generate the URL. + var bname = this.constructor.backendResourceName; + if (bname) { + return '/' + bname + '/'; + } + return null; + } + // The set of models has changed + + // Holds the fetch parameters + + }, { + key: 'initialize', + + + // Empty function, but can be overridden if you want to do something after initializing the model. + value: function initialize() {} + }, { + key: 'isLoading', + get: function get$$1() { + return this.__pendingRequestCount > 0; + } + }, { + key: 'length', + get: function get$$1() { + return this.models.length; + } + }, { + key: 'backendResourceName', + set: function set$$1(v) { + invariant(false, '`backendResourceName` should be a static property on the store.'); + } + }]); + + function Store() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + classCallCheck(this, Store); + + _initDefineProp(this, 'models', _descriptor, this); + + _initDefineProp(this, 'params', _descriptor2, this); + + _initDefineProp(this, '__pendingRequestCount', _descriptor3, this); + + _initDefineProp(this, '__setChanged', _descriptor4, this); + + _initDefineProp(this, '__state', _descriptor5, this); + + this.__activeRelations = []; + this.Model = null; + this.api = null; + + invariant(lodash.isPlainObject(options), 'Store only accepts an object with options. Chain `.parse(data)` to add models.'); + lodash.forIn(options, function (value, option) { + invariant(AVAILABLE_CONST_OPTIONS.includes(option), 'Unknown option passed to store: ' + option); + }); + this.__repository = options.repository; + if (options.relations) { + this.__parseRelations(options.relations); + } + if (options.limit !== undefined) { + this.setLimit(options.limit); + } + if (options.comparator) { + this.comparator = options.comparator; + } + if (options.params) { + this.params = options.params; + } + this.initialize(); + } + + createClass(Store, [{ + key: '__parseRelations', + value: function __parseRelations(activeRelations) { + this.__activeRelations = activeRelations; + } + }, { + key: '__getApi', + value: function __getApi() { + invariant(this.api, 'You are trying to perform a API request without an `api` property defined on the store.'); + invariant(lodash.result(this, 'url'), 'You are trying to perform a API request without an `url` property defined on the store.'); + return this.api; + } + }, { + key: 'fromBackend', + value: function fromBackend(_ref) { + var _this = this; + + var data = _ref.data, + repos = _ref.repos, + relMapping = _ref.relMapping, + reverseRelMapping = _ref.reverseRelMapping; + + invariant(data, 'Backend error. Data is not set. HINT: DID YOU FORGET THE M2M again?'); + + this.models.replace(data.map(function (record) { + // TODO: I'm not happy at all about how this looks. + // We'll need to finetune some things, but hey, for now it works. + var model = _this._newModel(); + model.fromBackend({ + data: record, + repos: repos, + relMapping: relMapping, + reverseRelMapping: reverseRelMapping + }); + return model; + })); + this.sort(); + } + }, { + key: '_newModel', + value: function _newModel() { + var model = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + + return new this.Model(model, { + store: this, + relations: this.__activeRelations + }); + } + }, { + key: 'sort', + value: function sort() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + invariant(lodash.isPlainObject(options), 'Expecting a plain object for options.'); + if (!this.comparator) { + return this; + } + if (typeof this.comparator === 'string') { + this.models.replace(this.sortBy(this.comparator)); + } else { + this.models.replace(this.models.slice().sort(this.comparator)); + } + return this; + } + }, { + key: 'parse', + value: function parse(models) { + invariant(lodash.isArray(models), 'Parameter supplied to `parse()` is not an array, got: ' + JSON.stringify(models)); + // Parse does not mutate __setChanged, as it is used in + // fromBackend in the model... + this.models.replace(models.map(this._newModel.bind(this))); + this.sort(); + + return this; + } + }, { + key: 'parseValidationErrors', + value: function parseValidationErrors(valErrors) { + this.each(function (model) { + model.parseValidationErrors(valErrors); + }); + } + }, { + key: 'clearValidationErrors', + value: function clearValidationErrors() { + this.each(function (model) { + model.clearValidationErrors(); + }); + } + }, { + key: 'add', + value: function add(models) { + var _this2 = this; + + var singular = !lodash.isArray(models); + models = singular ? [models] : models.slice(); + + var modelInstances = models.map(this._newModel.bind(this)); + + modelInstances.forEach(function (modelInstance) { + var primaryValue = modelInstance[_this2.Model.primaryKey]; + invariant(!primaryValue || !_this2.get(primaryValue), 'A model with the same primary key value "' + primaryValue + '" already exists in this store.'); + _this2.__setChanged = true; + _this2.models.push(modelInstance); + }); + this.sort(); + + return singular ? modelInstances[0] : modelInstances; + } + }, { + key: 'remove', + value: function remove(models) { + var _this3 = this; + + var singular = !lodash.isArray(models); + models = singular ? [models] : models.slice(); + + models.forEach(function (model) { + return _this3.models.remove(model); + }); + if (models.length > 0) { + this.__setChanged = true; + } + return models; + } + }, { + key: 'removeById', + value: function removeById(ids) { + var _this4 = this; + + var singular = !lodash.isArray(ids); + ids = singular ? [ids] : ids.slice(); + invariant(!ids.some(isNaN), 'Cannot remove a model by id that is not a number: ' + JSON.stringify(ids)); + + var models = ids.map(function (id) { + return _this4.get(id); + }); + + models.forEach(function (model) { + if (model) { + _this4.models.remove(model); + _this4.__setChanged = true; + } + }); + + return models; + } + }, { + key: 'clear', + value: function clear() { + var length = this.models.length; + this.models.clear(); + + if (length > 0) { + this.__setChanged = true; + } + } + }, { + key: 'buildFetchData', + value: function buildFetchData(options) { + return Object.assign(this.__getApi().buildFetchStoreParams(this), this.params, options.data); + } + }, { + key: 'fetch', + value: function fetch() { + var _this5 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + + var data = this.buildFetchData(options); + var promise = this.wrapPendingRequestCount(this.__getApi().fetchStore({ + url: options.url || lodash.result(this, 'url'), + data: data, + requestOptions: lodash.omit(options, 'data') + }).then(mobx.action(function (res) { + _this5.__state.totalRecords = res.totalRecords; + _this5.fromBackend(res); + + return res.response; + }))); + + return promise; + } + }, { + key: '__parseNewIds', + value: function __parseNewIds(idMaps) { + this.each(function (model) { + return model.__parseNewIds(idMaps); + }); + } + }, { + key: 'toJS', + value: function toJS() { + return this.models.map(function (model) { + return model.toJS(); + }); + } + + // Methods for pagination. + + }, { + key: 'getPageOffset', + value: function getPageOffset() { + return (this.__state.currentPage - 1) * this.__state.limit; + } + }, { + key: 'setLimit', + value: function setLimit(limit) { + invariant(!limit || Number.isInteger(limit), 'Page limit should be a number or falsy value.'); + this.__state.limit = limit || null; + } + }, { + key: 'getNextPage', + value: function getNextPage() { + invariant(this.hasNextPage, 'There is no next page.'); + this.__state.currentPage += 1; + return this.fetch(); + } + }, { + key: 'getPreviousPage', + value: function getPreviousPage() { + invariant(this.hasPreviousPage, 'There is no previous page.'); + this.__state.currentPage -= 1; + return this.fetch(); + } + }, { + key: 'setPage', + value: function setPage() { + var page = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + invariant(Number.isInteger(page) && page >= 1, 'Page should be a number above 1.'); + this.__state.currentPage = page; + if (options.fetch === undefined || options.fetch) { + return this.fetch(); + } + invariant( + // Always allow to go to page 1. + page <= (this.totalPages || 1), 'Page should be between 1 and ' + this.totalPages + '.'); + return Promise.resolve(); + } + }, { + key: 'clearSetChanges', + value: function clearSetChanges() { + this.__setChanged = false; + } + }, { + key: 'toBackendAll', + value: function toBackendAll() { + var _this6 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var relevantModels = options.onlyChanges ? this.models.filter(function (model) { + return model.isNew || model.hasUserChanges; + }) : this.models; + var modelData = relevantModels.map(function (model) { + return model.toBackendAll(options); + }); + + var data = []; + var relations = {}; + + modelData.forEach(function (model) { + data = data.concat(model.data); + lodash.forIn(model.relations, function (relModel, key) { + relations[key] = relations[key] ? relations[key].concat(relModel) : relModel; + // TODO: this primaryKey is not the primaryKey of the relation we're de-duplicating... + relations[key] = lodash.uniqBy(relations[key], _this6.Model.primaryKey); + }); + }); + + return { data: data, relations: relations }; + } + + // Create a new instance of this store with a predicate applied. + // This new store will be automatically kept in-sync with all models that adhere to the predicate. + + }, { + key: 'virtualStore', + value: function virtualStore(_ref2) { + var _this7 = this; + + var filter = _ref2.filter, + comparator = _ref2.comparator; + + var store = new this.constructor({ + relations: this.__activeRelations, + comparator: comparator + }); + + // Oh gawd MobX is so awesome. + var events = mobx.autorun(function () { + var models = _this7.filter(filter); + store.models.replace(models); + store.sort(); + + // When the parent store is busy, make sure the virtual store is + // also busy. + store.__pendingRequestCount = _this7.__pendingRequestCount; + }); + + store.unsubscribeVirtualStore = events; + + return store; + } + + // Helper methods to read models. + + }, { + key: 'get', + value: function get$$1(id) { + // The id can be defined as a string or int, but we want it to work in both cases. + return this.models.find(function (model) { + return model[model.constructor.primaryKey] == id; + } // eslint-disable-line eqeqeq + ); + } + }, { + key: 'getByIds', + value: function getByIds(ids) { + return this.models.filter(function (model) { + var id = model[model.constructor.primaryKey]; + return ids.includes(id) || ids.includes('' + id); + }); + } + }, { + key: 'map', + value: function map(predicate) { + return lodash.map(this.models, predicate); + } + }, { + key: 'mapByPrimaryKey', + value: function mapByPrimaryKey() { + return this.map(this.Model.primaryKey); + } + }, { + key: 'filter', + value: function filter(predicate) { + return lodash.filter(this.models, predicate); + } + }, { + key: 'find', + value: function find(predicate) { + return lodash.find(this.models, predicate); + } + }, { + key: 'each', + value: function each(predicate) { + return this.models.forEach(predicate); + } + }, { + key: 'forEach', + value: function forEach(predicate) { + return this.models.forEach(predicate); + } + }, { + key: 'sortBy', + value: function sortBy(iteratees) { + return lodash.sortBy(this.models, iteratees); + } + }, { + key: 'at', + value: function at(index) { + var zeroLength = this.length - 1; + invariant(index <= zeroLength, 'Index ' + index + ' is out of bounds (max ' + zeroLength + ').'); + if (index < 0) { + index += this.length; + } + return this.models[index]; + } + }, { + key: 'wrapPendingRequestCount', + value: function wrapPendingRequestCount(promise) { + var _this8 = this; + + this.__pendingRequestCount++; + + return promise.then(function (res) { + _this8.__pendingRequestCount--; + return res; + }).catch(function (err) { + _this8.__pendingRequestCount--; + throw err; + }); + } + }, { + key: 'saveAllFiles', + value: function saveAllFiles() { + var relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var promises = []; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = this.models[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var model = _step.value; + + promises.push(model.saveAllFiles(relations)); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return Promise.all(promises); + } + }, { + key: 'totalPages', + get: function get$$1() { + if (!this.__state.limit) { + return 0; + } + return Math.ceil(this.__state.totalRecords / this.__state.limit); + } + }, { + key: 'currentPage', + get: function get$$1() { + return this.__state.currentPage; + } + }, { + key: 'hasNextPage', + get: function get$$1() { + return this.__state.currentPage + 1 <= this.totalPages; + } + }, { + key: 'hasPreviousPage', + get: function get$$1() { + return this.__state.currentPage > 1; + } + }, { + key: 'hasUserChanges', + get: function get$$1() { + return this.hasSetChanges || this.models.some(function (m) { + return m.hasUserChanges; + }); + } + + // TODO: Maybe we can keep track of what got added and what got + // removed exactly. For now this should be enough. + + }, { + key: 'hasSetChanges', + get: function get$$1() { + return this.__setChanged; + } + }]); + return Store; +}(), _class2.backendResourceName = '', _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, 'models', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return []; + } +}), _descriptor2 = _applyDecoratedDescriptor(_class.prototype, 'params', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor3 = _applyDecoratedDescriptor(_class.prototype, '__pendingRequestCount', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return 0; + } +}), _descriptor4 = _applyDecoratedDescriptor(_class.prototype, '__setChanged', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return false; + } +}), _descriptor5 = _applyDecoratedDescriptor(_class.prototype, '__state', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return { + currentPage: 1, + limit: 25, + totalRecords: 0 + }; + } +}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype)), _class); + +var _class$1, _descriptor$1, _descriptor2$1, _descriptor3$1, _descriptor4$1, _descriptor5$1, _descriptor6, _descriptor7, _class2$1, _temp$1; + +function _initDefineProp$1(target, property, descriptor, context) { + if (!descriptor) return; + Object.defineProperty(target, property, { + enumerable: descriptor.enumerable, + configurable: descriptor.configurable, + writable: descriptor.writable, + value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 + }); +} + +function _applyDecoratedDescriptor$1(target, property, decorators, descriptor, context) { + var desc = {}; + Object['ke' + 'ys'](descriptor).forEach(function (key) { + desc[key] = descriptor[key]; + }); + desc.enumerable = !!desc.enumerable; + desc.configurable = !!desc.configurable; + + if ('value' in desc || desc.initializer) { + desc.writable = true; + } + + desc = decorators.slice().reverse().reduce(function (desc, decorator) { + return decorator(target, property, desc) || desc; + }, desc); + + if (context && desc.initializer !== void 0) { + desc.value = desc.initializer ? desc.initializer.call(context) : void 0; + desc.initializer = undefined; + } + + if (desc.initializer === void 0) { + Object['define' + 'Property'](target, property, desc); + desc = null; + } + + return desc; +} + +function concatInDict(dict, key, value) { + dict[key] = dict[key] ? dict[key].concat(value) : value; +} + +// Find the relation name before the first dot, and include all other relations after it +// Example: input `animal.kind.breed` output -> `['animal', 'kind.breed']` +var RE_SPLIT_FIRST_RELATION = /([^.]+)\.(.+)/; + +// TODO: find a way to get a list of existing properties automatically. +var FORBIDDEN_ATTRS = ['url', 'urlRoot', 'api', 'isNew', 'isLoading', 'parse', 'save', 'clear']; + +var Model = (_class$1 = (_temp$1 = _class2$1 = function () { + createClass(Model, [{ + key: 'urlRoot', + + // How the model is known at the backend. This is useful when the model is in a relation that has a different name. + value: function urlRoot() { + // Try to auto-generate the URL. + var bname = this.constructor.backendResourceName; + if (bname) { + return '/' + bname + '/'; + } + return null; + } + // Holds original attributes with values, so `clear()` knows what to reset to (quite ugly). + + // Holds activated - nested - relations (e.g. `['animal', 'animal.breed']`) + + // Holds activated - non-nested - relations (e.g. `['animal']`) + + // A `cid` can be used to identify the model locally. + + // URL query params that are added to fetch requests. + + // Holds fields (attrs+relations) that have been changed via setInput() + + + // File state + + }, { + key: 'wrapPendingRequestCount', + value: function wrapPendingRequestCount(promise) { + var _this = this; + + this.__pendingRequestCount++; + + return promise.then(function (res) { + _this.__pendingRequestCount--; + return res; + }).catch(function (err) { + _this.__pendingRequestCount--; + throw err; + }); + } + + // Useful to reference to this model in a relation - that is not yet saved to the backend. + + }, { + key: 'getNegativeId', + value: function getNegativeId() { + return -parseInt(this.cid.replace('m', '')); + } + }, { + key: 'getInternalId', + value: function getInternalId() { + if (this.isNew) { + return this.getNegativeId(); + } + return this[this.constructor.primaryKey]; + } + }, { + key: 'casts', + value: function casts() { + return {}; + } + }, { + key: 'fileFields', + value: function fileFields() { + return this.constructor.fileFields; + } + }, { + key: 'pickFields', + value: function pickFields() { + return this.constructor.pickFields; + } + }, { + key: 'omitFields', + value: function omitFields() { + return this.constructor.omitFields; + } + + // Empty function, but can be overridden if you want to do something after initializing the model. + + }, { + key: 'initialize', + value: function initialize() {} + }, { + key: 'url', + get: function get$$1() { + var id = this[this.constructor.primaryKey]; + return '' + lodash.result(this, 'urlRoot') + (id ? id + '/' : ''); + } + }, { + key: 'isNew', + get: function get$$1() { + return !this[this.constructor.primaryKey]; + } + }, { + key: 'isLoading', + get: function get$$1() { + return this.__pendingRequestCount > 0; + } + }, { + key: 'primaryKey', + set: function set$$1(v) { + invariant(false, '`primaryKey` should be a static property on the model.'); + } + }, { + key: 'backendResourceName', + set: function set$$1(v) { + invariant(false, '`backendResourceName` should be a static property on the model.'); + } + }]); + + function Model(data) { + var _this2 = this; + + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + classCallCheck(this, Model); + this.__attributes = []; + this.__originalAttributes = {}; + this.__activeRelations = []; + this.__activeCurrentRelations = []; + this.api = null; + this.cid = 'm' + lodash.uniqueId(); + + _initDefineProp$1(this, '__backendValidationErrors', _descriptor$1, this); + + _initDefineProp$1(this, '__pendingRequestCount', _descriptor2$1, this); + + _initDefineProp$1(this, '__fetchParams', _descriptor3$1, this); + + _initDefineProp$1(this, '__changes', _descriptor4$1, this); + + _initDefineProp$1(this, '__fileChanges', _descriptor5$1, this); + + _initDefineProp$1(this, '__fileDeletions', _descriptor6, this); + + _initDefineProp$1(this, '__fileExists', _descriptor7, this); + + this.__store = options.store; + this.__repository = options.repository; + // Find all attributes. Not all observables are an attribute. + lodash.forIn(this, function (value, key) { + if (!key.startsWith('__') && mobx.isObservableProp(_this2, key)) { + invariant(!FORBIDDEN_ATTRS.includes(key), 'Forbidden attribute key used: `' + key + '`'); + _this2.__attributes.push(key); + var newValue = value; + // An array or object observable can be mutated, so we want to ensure we always have + // the original not-yet-mutated object/array. + if (mobx.isObservableArray(value)) { + newValue = value.slice(); + } else if (mobx.isObservableObject(value)) { + newValue = Object.assign({}, value); + } + _this2.__originalAttributes[key] = newValue; + } + }); + if (options.relations) { + this.__parseRelations(options.relations); + } + if (data) { + this.parse(data); + } + this.initialize(); + + this.saveFile = this.saveFile.bind(this); + } + + createClass(Model, [{ + key: '__parseRelations', + value: function __parseRelations(activeRelations) { + var _this3 = this; + + this.__activeRelations = activeRelations; + // TODO: No idea why getting the relations only works when it's a Function. + var relations = this.relations && this.relations(); + var relModels = {}; + activeRelations.forEach(function (aRel) { + // If aRel is null, this relation is already defined by another aRel + // IE.: town.restaurants.chef && town + if (aRel === null || !!_this3[aRel]) { + return; + } + var relNames = aRel.match(RE_SPLIT_FIRST_RELATION); + + var currentRel = relNames ? relNames[1] : aRel; + var otherRelNames = relNames && relNames[2]; + var currentProp = relModels[currentRel]; + var otherRels = otherRelNames && [otherRelNames]; + + // When two nested relations are defined next to each other (e.g. `['kind.breed', 'kind.location']`), + // the relation `kind` only needs to be initialized once. + relModels[currentRel] = currentProp ? currentProp.concat(otherRels) : otherRels; + invariant(!_this3.__attributes.includes(currentRel), 'Cannot define `' + currentRel + '` as both an attribute and a relation. You probably need to remove the attribute.'); + if (!_this3.__activeCurrentRelations.includes(currentRel)) { + _this3.__activeCurrentRelations.push(currentRel); + } + }); + // extendObservable where we omit the fields that are already created from other relations + mobx.extendObservable(this, lodash.mapValues(lodash.omit(relModels, Object.keys(relModels).filter(function (rel) { + return !!_this3[rel]; + })), function (otherRelNames, relName) { + var RelModel = relations[relName]; + invariant(RelModel, 'Specified relation "' + relName + '" does not exist on model.'); + var options = { relations: otherRelNames }; + if (RelModel.prototype instanceof Store) { + return new RelModel(options); + } + return new RelModel(null, options); + })); + } + + // Many backends use snake_case for attribute names, so we convert to snake_case by default. + + }, { + key: 'clearUserFieldChanges', + value: function clearUserFieldChanges() { + this.__changes.clear(); + } + }, { + key: 'clearUserFileChanges', + value: function clearUserFileChanges() { + this.__fileChanges = {}; + this.__fileDeletions = {}; + this.__fileExists = {}; + } + }, { + key: 'clearUserChanges', + value: function clearUserChanges() { + this.clearUserFieldChanges(); + this.clearUserFileChanges(); + } + }, { + key: 'toBackend', + value: function toBackend() { + var _this4 = this; + + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var _ref$data = _ref.data, + data = _ref$data === undefined ? {} : _ref$data, + _ref$mapData = _ref.mapData, + mapData = _ref$mapData === undefined ? function (x) { + return x; + } : _ref$mapData, + options = objectWithoutProperties(_ref, ['data', 'mapData']); + + var output = {}; + // By default we'll include all fields (attributes+relations), but sometimes you might want to specify the fields to be included. + var fieldFilter = function fieldFilter(field) { + if (!_this4.fieldFilter(field)) { + return false; + } + if (options.fields) { + return options.fields.includes(field); + } + if (!_this4.isNew && options.onlyChanges) { + var forceFields = options.forceFields || []; + return forceFields.includes(field) || _this4.__changes.includes(field) || _this4[field] instanceof Store && _this4[field].hasSetChanges || + // isNew is always true for relations that haven't been saved. + // If no property has been tweaked, its id serializes as null. + // So, we need to skip saving the id if new and no changes. + _this4[field] instanceof Model && _this4[field].isNew && _this4[field].hasUserChanges; + } + return true; + }; + this.__attributes.filter(fieldFilter).forEach(function (attr) { + if (!attr.startsWith('_')) { + output[_this4.constructor.toBackendAttrKey(attr)] = _this4.__toJSAttr(attr, _this4[attr]); + } + }); + + // Primary key is always forced to be included. + output[this.constructor.primaryKey] = this[this.constructor.primaryKey]; + + // Add active relations as id. + this.__activeCurrentRelations.filter(fieldFilter).forEach(function (currentRel) { + var rel = _this4[currentRel]; + var relBackendName = _this4.constructor.toBackendAttrKey(currentRel); + if (rel instanceof Model) { + output[relBackendName] = rel[rel.constructor.primaryKey]; + } + if (rel instanceof Store) { + output[relBackendName] = rel.mapByPrimaryKey(); + } + }); + + Object.assign(output, data); + return mapData(output); + } + }, { + key: 'toBackendAll', + value: function toBackendAll() { + var _this5 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var nestedRelations = options.nestedRelations || {}; + var data = this.toBackend({ + data: options.data, + mapData: options.mapData, + onlyChanges: options.onlyChanges + }); + + if (data[this.constructor.primaryKey] === null) { + data[this.constructor.primaryKey] = this.getNegativeId(); + } + + var relations = {}; + + this.__activeCurrentRelations.forEach(function (currentRel) { + var rel = _this5[currentRel]; + var relBackendName = _this5.constructor.toBackendAttrKey(currentRel); + var subRelations = nestedRelations[currentRel]; + + if (subRelations !== undefined) { + if (data[relBackendName] === null) { + data[relBackendName] = rel.getNegativeId(); + } else if (lodash.isArray(data[relBackendName])) { + data[relBackendName] = lodash.uniq(data[relBackendName].map(function (pk, i) { + return pk === null ? rel.at(i).getNegativeId() : pk; + })); + } else if (options.onlyChanges && !rel.hasUserChanges) { + return; + } + + var relBackendData = rel.toBackendAll({ + nestedRelations: subRelations, + onlyChanges: options.onlyChanges + }); + + // Sometimes the backend knows the relation by a different name, e.g. the relation is called + // `activities`, but the name in the backend is `activity`. + // In that case, you can add `static backendResourceName = 'activity';` to that model. + var realBackendName = rel.constructor.backendResourceName || relBackendName; + + if (relBackendData.data.length > 0) { + concatInDict(relations, realBackendName, relBackendData.data); + + // De-duplicate relations based on `primaryKey`. + // TODO: Avoid serializing recursively multiple times in the first place? + // TODO: What if different relations have different "freshness"? + relations[realBackendName] = lodash.uniqBy(relations[realBackendName], rel.constructor.primaryKey || rel.Model.primaryKey); + } + + // There could still be changes in nested relations, + // include those anyway! + lodash.forIn(relBackendData.relations, function (relB, key) { + concatInDict(relations, key, relB); + }); + } + }); + + return { data: [data], relations: relations }; + } + + /** + * Makes this model a copy of the specified model + * or returns a copy of the current model when no model to copy is given + * It also clones the changes that were in the specified model. + * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. + * Cloning + * + * @param source {Model} - The model that should be copied + * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} + */ + + }, { + key: 'copy', + value: function copy() { + var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; + + var copiedModel = void 0; + // If our source is not a model it is 'probably' the options + if (source !== undefined && !(source instanceof Model)) { + options = source; + source = undefined; + } + + // Make sure that we have the correct model + if (source === undefined) { + source = this; + copiedModel = new source.constructor({ relations: source.__activeRelations }); + } else if (this.constructor !== source.constructor) { + copiedModel = new source.constructor({ relations: source.__activeRelations }); + } else { + copiedModel = this; + } + + var copyChanges = options.copyChanges; + + // Maintain the relations after copy + // this.__activeRelations = source.__activeRelations; + copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; + + copiedModel.__parseRelations(source.__activeRelations); + // Copy all fields and values from the specified model + copiedModel.parse(source.toJS()); + + // Set only the changed attributes + if (copyChanges) { + copiedModel._copyChanges(source); + } + + return copiedModel; + } + + /** + * Goes over model and all related models to set the changed values and notify the store + * + * @param source - the model to copy + * @param store - the store of the current model, to setChanged if there are changes + * @private + */ + + }, { + key: '_copyChanges', + value: function _copyChanges(source, store) { + var _this6 = this; + + // Maintain the relations after copy + this.__activeRelations = source.__activeRelations; + this.__activeCurrentRelations = source.__activeCurrentRelations; + + // Copy all changed fields and notify the store that there are changes + if (source.__changes.length > 0) { + if (store) { + store.__setChanged = true; + } else if (this.__store) { + this.__store.__setChanged = true; + } + + source.__changes.forEach(function (changedAttribute) { + _this6.setInput(changedAttribute, source[changedAttribute]); + }); + } + // Undefined safety + if (source.__activeCurrentRelations.length > 0) { + // Set the changes for all related models with changes + source.__activeCurrentRelations.forEach(function (relation) { + if (relation && source[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); + }); + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); + } + } + } + }); + } + } + }, { + key: 'toJS', + value: function toJS() { + var _this7 = this; + + var output = {}; + this.__attributes.forEach(function (attr) { + output[attr] = _this7.__toJSAttr(attr, _this7[attr]); + }); + + this.__activeCurrentRelations.forEach(function (currentRel) { + var model = _this7[currentRel]; + if (model) { + output[currentRel] = model.toJS(); + } + }); + return output; + } + }, { + key: '__toJSAttr', + value: function __toJSAttr(attr, value) { + var casts = this.casts(); + var cast = casts[attr]; + if (cast !== undefined) { + return mobx.toJS(cast.toJS(attr, value)); + } + return mobx.toJS(value); + } + }, { + key: 'setFetchParams', + value: function setFetchParams(params) { + this.__fetchParams = Object.assign({}, params); + } + }, { + key: '__parseRepositoryToData', + value: function __parseRepositoryToData(key, repository) { + if (lodash.isArray(key)) { + return lodash.filter(repository, function (m) { + return key.includes(m.id); + }); + } + return lodash.find(repository, { id: key }); + } + }, { + key: '__parseReverseRepositoryToData', + value: function __parseReverseRepositoryToData(reverseKeyName, key, repository) { + var searchKey = {}; + searchKey[reverseKeyName] = key; + return lodash.filter(repository, searchKey); + } + + /** + * We handle the fromBackend recursively. + * But when recursing, we don't send the full repository, we need to only send the repo + * relevant to the relation. + * + * So when we have a customer with a town.restaurants relation, + * we get a "town.restaurants": "restaurant", relMapping from Binder + * + * Here we create a scoped repository. + * The root gets a `town.restaurants` repo, but the `town` relation only gets the `restaurants` repo + */ + + }, { + key: '__scopeBackendResponse', + value: function __scopeBackendResponse(_ref2) { + var _this8 = this; + + var data = _ref2.data, + targetRelName = _ref2.targetRelName, + repos = _ref2.repos, + mapping = _ref2.mapping, + reverseMapping = _ref2.reverseMapping; + + var scopedData = null; + var relevant = false; + var scopedRepos = {}; + var scopedRelMapping = {}; + var scopedReverseRelMapping = {}; + + if (!data) { + return null; + } + + lodash.forIn(mapping, function (repoName, backendRelName) { + var repository = repos[repoName]; + // For backwards compatibility, reverseMapping is optional (for now) + var reverseRelName = reverseMapping ? reverseMapping[backendRelName] : null; + var relName = _this8.constructor.fromBackendAttrKey(backendRelName); + + if (targetRelName === relName) { + var relKey = data[_this8.constructor.toBackendAttrKey(relName)]; + if (relKey !== undefined) { + relevant = true; + scopedData = _this8.__parseRepositoryToData(relKey, repository); + } else if (repository && reverseRelName) { + var pk = data[_this8.constructor.primaryKey]; + relevant = true; + scopedData = _this8.__parseReverseRepositoryToData(reverseRelName, pk, repository); + if (_this8.relations(relName).prototype instanceof Model) { + if (scopedData.length === 0) { + scopedData = null; + } else if (scopedData.length === 1) { + scopedData = scopedData[0]; + } else { + throw new Error('multiple models found for related model'); + } + } + } + return; + } + + if (relName.startsWith(targetRelName + '.')) { + // If we have town.restaurants and the targetRel = town + // we need "restaurants" in the repository + relevant = true; + var backendRelNames = backendRelName.match(RE_SPLIT_FIRST_RELATION); + var scopedBackendRelName = backendRelNames[2]; + scopedRepos[repoName] = repository; + scopedRelMapping[scopedBackendRelName] = repoName; + scopedReverseRelMapping[scopedBackendRelName] = reverseMapping ? reverseMapping[backendRelName] : null; + } + }); + + if (!relevant) { + return null; + } + + return { scopedData: scopedData, scopedRepos: scopedRepos, scopedRelMapping: scopedRelMapping, scopedReverseRelMapping: scopedReverseRelMapping }; + } + + // `data` contains properties for the current model. + // `repos` is an object of "repositories". A repository is + // e.g. "animal_kind", while the relation name would be "kind". + // `relMapping` maps relation names to repositories. + + }, { + key: 'fromBackend', + value: function fromBackend(_ref3) { + var _this9 = this; + + var data = _ref3.data, + repos = _ref3.repos, + relMapping = _ref3.relMapping, + reverseRelMapping = _ref3.reverseRelMapping; + + // We handle the fromBackend recursively. On each relation of the source model + // fromBackend gets called as well, but with data scoped for itself + // + // So when we have a model with a `town.restaurants.chef` relation, + // we call fromBackend on the `town` relation. + lodash.each(this.__activeCurrentRelations, function (relName) { + var rel = _this9[relName]; + var resScoped = _this9.__scopeBackendResponse({ + data: data, + targetRelName: relName, + repos: repos, + mapping: relMapping, + reverseMapping: reverseRelMapping + }); + + // Make sure we don't parse every relation for nothing + if (!resScoped) { + return; + } + + var scopedData = resScoped.scopedData, + scopedRepos = resScoped.scopedRepos, + scopedRelMapping = resScoped.scopedRelMapping, + scopedReverseRelMapping = resScoped.scopedReverseRelMapping; + + rel.fromBackend({ + data: scopedData, + repos: scopedRepos, + relMapping: scopedRelMapping, + reverseRelMapping: scopedReverseRelMapping + }); + }); + + // Now all repositories are set on the relations, start parsing the actual data. + // `parse()` will recursively fill in all relations. + if (data) { + this.parse(data); + } + } + }, { + key: '__getApi', + value: function __getApi() { + invariant(this.api, 'You are trying to perform a API request without an `api` property defined on the model.'); + invariant(lodash.result(this, 'urlRoot'), 'You are trying to perform a API request without an `urlRoot` property defined on the model.'); + return this.api; + } + }, { + key: 'parse', + value: function parse(data) { + var _this10 = this; + + invariant(lodash.isPlainObject(data), 'Parameter supplied to `parse()` is not an object, got: ' + JSON.stringify(data)); + + lodash.forIn(data, function (value, key) { + var attr = _this10.constructor.fromBackendAttrKey(key); + if (_this10.__attributes.includes(attr)) { + _this10[attr] = _this10.__parseAttr(attr, value); + } else if (_this10.__activeCurrentRelations.includes(attr)) { + // In Binder, a relation property is an `int` or `[int]`, referring to its ID. + // However, it can also be an object if there are nested relations (non flattened). + if (lodash.isPlainObject(value) || lodash.isPlainObject(lodash.get(value, '[0]'))) { + _this10[attr].parse(value); + } else if (value === null) { + // The relation is cleared. + _this10[attr].clear(); + } + } + }); + + return this; + } + }, { + key: '__parseAttr', + value: function __parseAttr(attr, value) { + var casts = this.casts(); + var cast = casts[attr]; + if (cast !== undefined) { + return cast.parse(attr, value); + } + return value; + } + }, { + key: 'saveFile', + value: function saveFile(name) { + var _this11 = this; + + var snakeName = camelToSnake(name); + + if (this.__fileChanges[name]) { + var file = this.__fileChanges[name]; + + var data = new FormData(); + data.append(name, file, file.name); + + return this.api.post('' + this.url + snakeName + '/', data, { headers: { 'Content-Type': 'multipart/form-data' } }).then(mobx.action(function (res) { + _this11.__fileExists[name] = true; + delete _this11.__fileChanges[name]; + _this11.saveFromBackend(res); + })); + } else if (this.__fileDeletions[name]) { + if (this.__fileExists[name]) { + return this.api.delete('' + this.url + snakeName + '/').then(mobx.action(function () { + _this11.__fileExists[name] = false; + delete _this11.__fileDeletions[name]; + _this11.saveFromBackend({ data: defineProperty({}, snakeName, null) }); + })); + } else { + delete this.__fileDeletions[name]; + } + } else { + return Promise.resolve(); + } + } + }, { + key: 'saveFiles', + value: function saveFiles() { + return Promise.all(this.fileFields().filter(this.fieldFilter).map(this.saveFile)); + } + + /** + * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal save request, example: {onlyChanges: true} + */ + + }, { + key: 'validate', + value: function validate() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate parameter + if (options.params) { + options.params.validate = true; + } else { + options.params = { validate: true }; + } + return this.save(options); + } + }, { + key: 'save', + value: function save() { + var _this12 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.clearValidationErrors(); + return this.wrapPendingRequestCount(this.__getApi().saveModel({ + url: options.url || this.url, + data: this.toBackend({ + data: options.data, + mapData: options.mapData, + fields: options.fields, + onlyChanges: options.onlyChanges + }), + isNew: this.isNew, + requestOptions: lodash.omit(options, 'url', 'data', 'mapData') + }).then(mobx.action(function (res) { + // Only update the model when we are actually trying to save + if (!options.params || !options.params.validate) { + _this12.saveFromBackend(_extends({}, res, { + data: lodash.omit(res.data, _this12.fileFields().map(camelToSnake)) + })); + _this12.clearUserFieldChanges(); + return _this12.saveFiles().then(function () { + _this12.clearUserFileChanges(); + return Promise.resolve(res); + }); + } + })).catch(mobx.action(function (err) { + if (err.valErrors) { + _this12.parseValidationErrors(err.valErrors); + } + throw err; + }))); + } + }, { + key: 'setInput', + value: function setInput(name, value) { + invariant(this.__attributes.includes(name) || this.__activeCurrentRelations.includes(name), 'Field `' + name + '` does not exist on the model.'); + if (this.fileFields().includes(name)) { + if (this.__fileExists[name] === undefined) { + this.__fileExists[name] = this[name] !== null; + } + if (value) { + this.__fileChanges[name] = value; + delete this.__fileDeletions[name]; + + value = URL.createObjectURL(value) + '?content_type=' + value.type; + } else { + if (!this.__fileChanges[name] || this.__fileChanges[name].existed) { + this.__fileDeletions[name] = true; + } + delete this.__fileChanges[name]; + + value = null; + } + } + if (!this.__changes.includes(name)) { + this.__changes.push(name); + } + if (this.__activeCurrentRelations.includes(name)) { + if (lodash.isArray(value)) { + this[name].clear(); + this[name].add(value.map(function (v) { + return v.toJS(); + })); + } else if (value) { + this[name].parse(value.toJS()); + } else { + this[name].clear(); + } + } else { + this[name] = value; + } + if (this.backendValidationErrors[name]) { + this.__backendValidationErrors = Object.assign(this.backendValidationErrors, defineProperty({}, name, undefined)); + } + } + }, { + key: 'saveAllFiles', + value: function saveAllFiles() { + var relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var promises = [this.saveFiles()]; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = Object.keys(relations)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var rel = _step.value; + + promises.push(this[rel].saveAllFiles(relations[rel])); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return Promise.all(promises); + } + + /** + * Validates a model and relations by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} + */ + + }, { + key: 'validateAll', + value: function validateAll() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate option + if (options.params) { + options.params.validate = true; + } else { + options.params = { validate: true }; + } + return this.saveAll(options); + } + }, { + key: 'saveAll', + value: function saveAll() { + var _this13 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.clearValidationErrors(); + return this.wrapPendingRequestCount(this.__getApi().saveAllModels({ + url: lodash.result(this, 'urlRoot'), + model: this, + data: this.toBackendAll({ + data: options.data, + mapData: options.mapData, + nestedRelations: relationsToNestedKeys(options.relations || []), + onlyChanges: options.onlyChanges + }), + requestOptions: lodash.omit(options, 'relations', 'data', 'mapData') + }).then(mobx.action(function (res) { + // Only update the models if we are actually trying to save + if (!options.params || !options.params.validate) { + _this13.saveFromBackend(res); + _this13.clearUserFieldChanges(); + + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); + } + }); + + return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + _this13.clearUserFileChanges(); + + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFileChanges(); + } + }); + + return res; + }); + } + })).catch(mobx.action(function (err) { + if (err.valErrors) { + _this13.parseValidationErrors(err.valErrors); + } + throw err; + }))); + } + + // After saving a model, we should get back an ID mapping from the backend which looks like: + // `{ "animal": [[-1, 10]] }` + + }, { + key: '__parseNewIds', + value: function __parseNewIds(idMaps) { + var _this14 = this; + + var bName = this.constructor.backendResourceName; + if (bName && idMaps[bName]) { + var idMap = idMaps[bName].find(function (ids) { + return ids[0] === _this14.getInternalId(); + }); + if (idMap) { + this[this.constructor.primaryKey] = idMap[1]; + } + } + lodash.each(this.__activeCurrentRelations, function (relName) { + var rel = _this14[relName]; + rel.__parseNewIds(idMaps); + }); + } + }, { + key: 'validationErrorFormatter', + value: function validationErrorFormatter(obj) { + return obj.code; + } + }, { + key: 'parseValidationErrors', + value: function parseValidationErrors(valErrors) { + var _this15 = this; + + var bname = this.constructor.backendResourceName; + + if (valErrors[bname]) { + var id = this.getInternalId(); + // When there is no id or negative id, the backend may use the string 'null'. Bit weird, but eh. + var errorsForModel = valErrors[bname][id] || valErrors[bname]['null']; + if (errorsForModel) { + var camelCasedErrors = lodash.mapKeys(errorsForModel, function (value, key) { + return snakeToCamel(key); + }); + var formattedErrors = lodash.mapValues(camelCasedErrors, function (valError) { + return valError.map(_this15.validationErrorFormatter); + }); + this.__backendValidationErrors = formattedErrors; + } + } + + this.__activeCurrentRelations.forEach(function (currentRel) { + _this15[currentRel].parseValidationErrors(valErrors); + }); + } + }, { + key: 'clearValidationErrors', + value: function clearValidationErrors() { + var _this16 = this; + + this.__backendValidationErrors = {}; + this.__activeCurrentRelations.forEach(function (currentRel) { + _this16[currentRel].clearValidationErrors(); + }); + } + + // This is just a pass-through to make it easier to override parsing backend responses from the backend. + // Sometimes the backend won't return the model after a save because e.g. it is created async. + + }, { + key: 'saveFromBackend', + value: function saveFromBackend(res) { + return this.fromBackend(res); + } + + // TODO: This is a bit hacky... + + }, { + key: 'delete', + value: function _delete() { + var _this17 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var removeFromStore = function removeFromStore() { + return _this17.__store ? _this17.__store.remove(_this17) : null; + }; + if (options.immediate || this.isNew) { + removeFromStore(); + } + if (this.isNew) { + return Promise.resolve(); + } + + return this.wrapPendingRequestCount(this.__getApi().deleteModel({ + url: options.url || this.url, + requestOptions: lodash.omit(options, ['immediate', 'url']) + }).then(mobx.action(function () { + if (!options.immediate) { + removeFromStore(); + } + }))); + } + }, { + key: 'buildFetchData', + value: function buildFetchData(options) { + return Object.assign(this.__getApi().buildFetchModelParams(this), this.__fetchParams, options.data); + } + }, { + key: 'fetch', + value: function fetch() { + var _this18 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + invariant(!this.isNew, 'Trying to fetch model without id!'); + + var data = this.buildFetchData(options); + var promise = this.wrapPendingRequestCount(this.__getApi().fetchModel({ + url: options.url || this.url, + data: data, + requestOptions: lodash.omit(options, ['data', 'url']) + }).then(mobx.action(function (res) { + _this18.fromBackend(res); + }))); + + return promise; + } + }, { + key: 'clear', + value: function clear() { + var _this19 = this; + + lodash.forIn(this.__originalAttributes, function (value, key) { + _this19[key] = value; + }); + + this.__activeCurrentRelations.forEach(function (currentRel) { + _this19[currentRel].clear(); + }); + } + }, { + key: 'hasUserChanges', + get: function get$$1() { + var _this20 = this; + + if (this.__changes.length > 0) { + return true; + } + return this.__activeCurrentRelations.some(function (rel) { + return _this20[rel].hasUserChanges; + }); + } + }, { + key: 'fieldFilter', + get: function get$$1() { + var pickFields = this.pickFields(); + var omitFields = this.omitFields(); + + return function (name) { + return (!pickFields || pickFields.includes(name)) && !omitFields.includes(name); + }; + } + }, { + key: 'backendValidationErrors', + get: function get$$1() { + return this.__backendValidationErrors; + } + }], [{ + key: 'toBackendAttrKey', + value: function toBackendAttrKey(attrKey) { + return camelToSnake(attrKey); + } + + // In the frontend we don't want to deal with those snake_case attr names. + + }, { + key: 'fromBackendAttrKey', + value: function fromBackendAttrKey(attrKey) { + return snakeToCamel(attrKey); + } + }]); + return Model; +}(), _class2$1.primaryKey = 'id', _class2$1.backendResourceName = '', _class2$1.fileFields = [], _class2$1.pickFields = undefined, _class2$1.omitFields = [], _temp$1), (_descriptor$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__backendValidationErrors', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor2$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__pendingRequestCount', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return 0; + } +}), _descriptor3$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fetchParams', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor4$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__changes', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return []; + } +}), _descriptor5$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileChanges', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor6 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileDeletions', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor7 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileExists', [mobx.observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _applyDecoratedDescriptor$1(_class$1.prototype, 'url', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'url'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isNew', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isNew'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isLoading'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '__parseRelations', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '__parseRelations'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'hasUserChanges'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fieldFilter', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fieldFilter'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fromBackend'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parse'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'save', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'save'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'setInput', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'saveAll', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'saveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parseValidationErrors', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parseValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clearValidationErrors', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clearValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'backendValidationErrors', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'backendValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'delete', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'delete'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fetch'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clear'), _class$1.prototype)), _class$1); + +// Function ripped from Django docs. +// See: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax +function csrfSafeMethod(method) { + // These HTTP methods do not require CSRF protection. + return (/^(GET|HEAD|OPTIONS|TRACE)$/i.test(method) + ); +} + +var BinderApi = function () { + function BinderApi() { + classCallCheck(this, BinderApi); + this.baseUrl = null; + this.csrfToken = null; + this.defaultHeaders = {}; + this.axios = axios.create(); + + this.__initializeCsrfHandling(); + } + + createClass(BinderApi, [{ + key: '__initializeCsrfHandling', + value: function __initializeCsrfHandling() { + var _this = this; + + this.axios.interceptors.response.use(null, function (err) { + var status = lodash.get(err, 'response.status'); + var statusErrCode = lodash.get(err, 'response.data.code'); + var doNotRetry = lodash.get(err, 'response.config.doNotRetry'); + if (status === 403 && statusErrCode === 'CSRFFailure' && !doNotRetry) { + return _this.fetchCsrfToken().then(function () { + return _this.axios(_extends({}, err.response.config, { + doNotRetry: true + })); + }); + } + return Promise.reject(err); + }); + } + }, { + key: '__request', + value: function __request(method, url, data, options) { + options || (options = {}); + var useCsrfToken = csrfSafeMethod(method) ? undefined : this.csrfToken; + this.__testUrl(url); + + var axiosOptions = { + method: method, + baseURL: this.baseUrl, + url: url, + data: method !== 'get' && data ? data : undefined, + params: method === 'get' && data ? data : options.params + }; + + Object.assign(axiosOptions, options); + + // Don't clear existing headers when adding `options.headers` + var headers = Object.assign({ + 'Content-Type': 'application/json', + 'X-Csrftoken': useCsrfToken + }, this.defaultHeaders, options.headers); + axiosOptions.headers = headers; + + var xhr = this.axios(axiosOptions); + + // We fork the promise tree as we want to have the error traverse to the listeners + if (this.onRequestError && options.skipRequestError !== true) { + xhr.catch(this.onRequestError); + } + + var onSuccess = options.skipFormatter === true ? Promise.resolve() : this.__responseFormatter; + return xhr.then(onSuccess); + } + }, { + key: 'parseBackendValidationErrors', + value: function parseBackendValidationErrors(response) { + var valErrors = lodash.get(response, 'data.errors'); + if (response.status === 400 && valErrors) { + return valErrors; + } + return null; + } + }, { + key: 'fetchCsrfToken', + value: function fetchCsrfToken() { + var _this2 = this; + + return this.get('/api/bootstrap/').then(function (res) { + _this2.csrfToken = res.csrf_token; + }); + } + }, { + key: '__responseFormatter', + value: function __responseFormatter(res) { + return res.data; + } + }, { + key: '__testUrl', + value: function __testUrl(url) { + if (!url.endsWith('/')) { + throw new Error('Binder does not accept urls that do not have a trailing slash: ' + url); + } + } + }, { + key: 'get', + value: function get$$1(url, data, options) { + return this.__request('get', url, data, options); + } + }, { + key: 'post', + value: function post(url, data, options) { + return this.__request('post', url, data, options); + } + }, { + key: 'patch', + value: function patch(url, data, options) { + return this.__request('patch', url, data, options); + } + }, { + key: 'put', + value: function put(url, data, options) { + return this.__request('put', url, data, options); + } + }, { + key: 'delete', + value: function _delete(url, data, options) { + return this.__request('delete', url, data, options); + } + }, { + key: 'buildFetchModelParams', + value: function buildFetchModelParams(model) { + return { + // TODO: I really dislike that this is comma separated and not an array. + // We should fix this in the Binder API. + with: model.__activeRelations.map(model.constructor.toBackendAttrKey).join(',') || null + }; + } + }, { + key: 'fetchModel', + value: function fetchModel(_ref) { + var url = _ref.url, + data = _ref.data, + requestOptions = _ref.requestOptions; + + return this.get(url, data, requestOptions).then(function (res) { + return { + data: res.data, + repos: res.with, + relMapping: res.with_mapping, + reverseRelMapping: res.with_related_name_mapping + }; + }); + } + }, { + key: 'saveModel', + value: function saveModel(_ref2) { + var _this3 = this; + + var url = _ref2.url, + data = _ref2.data, + isNew = _ref2.isNew, + requestOptions = _ref2.requestOptions; + + var method = isNew ? 'post' : 'patch'; + return this[method](url, data, requestOptions).then(function (newData) { + return { data: newData }; + }).catch(function (err) { + if (err.response) { + err.valErrors = _this3.parseBackendValidationErrors(err.response); + } + throw err; + }); + } + }, { + key: 'saveAllModels', + value: function saveAllModels(_ref3) { + var _this4 = this; + + var url = _ref3.url, + data = _ref3.data, + model = _ref3.model, + requestOptions = _ref3.requestOptions; + + return this.put(url, { + data: data.data, + with: data.relations + }, requestOptions).then(function (res) { + if (res.idmap) { + model.__parseNewIds(res.idmap); + } + return res; + }).catch(function (err) { + if (err.response) { + err.valErrors = _this4.parseBackendValidationErrors(err.response); + } + throw err; + }); + } + }, { + key: 'deleteModel', + value: function deleteModel(_ref4) { + var url = _ref4.url, + requestOptions = _ref4.requestOptions; + + // TODO: kind of silly now, but we'll probably want better error handling soon. + return this.delete(url, null, requestOptions); + } + }, { + key: 'buildFetchStoreParams', + value: function buildFetchStoreParams(store) { + var offset = store.getPageOffset(); + var limit = store.__state.limit; + return { + with: store.__activeRelations.map(store.Model.toBackendAttrKey).join(',') || null, + limit: limit === null ? 'none' : limit, + // Hide offset if zero so the request looks cleaner in DevTools. + offset: offset || null + }; + } + }, { + key: 'fetchStore', + value: function fetchStore(_ref5) { + var url = _ref5.url, + data = _ref5.data, + requestOptions = _ref5.requestOptions; + + return this.get(url, data, requestOptions).then(function (res) { + return { + response: res, + data: res.data, + repos: res.with, + relMapping: res.with_mapping, + reverseRelMapping: res.with_related_name_mapping, + totalRecords: res.meta.total_records + }; + }); + } + }]); + return BinderApi; +}(); + +var DATE_LIB = 'moment'; +var SUPPORTED_DATE_LIBS = ['moment', 'luxon']; + +function configureDateLib(dateLib) { + invariant(SUPPORTED_DATE_LIBS.includes(dateLib), 'Unsupported date lib `' + dateLib + '`. ' + ('(Supported: ' + SUPPORTED_DATE_LIBS.map(function (dateLib) { + return '`' + dateLib + '`'; + }).join(', ') + ')')); + DATE_LIB = dateLib; +} + +function checkMomentInstance(attr, value) { + invariant(moment.isMoment(value), 'Attribute `' + attr + '` is not a moment instance.'); +} + +function checkLuxonDateTime(attr, value) { + invariant(moment.isMoment(value), 'Attribute `' + attr + '` is not a luxon DateTime.'); +} + +var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; +var LUXON_DATETIME_FORMAT = 'yyy-LL-ddTHH:mm:ssZZZ'; + +var CASTS = { + momentDate: { + parse: function parse(attr, value) { + if (value === null || value === undefined) { + return null; + } + return moment(value, 'YYYY-MM-DD'); + }, + toJS: function toJS(attr, value) { + if (value === null || value === undefined) { + return null; + } + checkMomentInstance(attr, value); + return value.format('YYYY-MM-DD'); + }, + + dateLib: 'moment' + }, + momentDatetime: { + parse: function parse(attr, value) { + if (value === null) { + return null; + } + return moment(value); + }, + toJS: function toJS(attr, value) { + if (value === null) { + return null; + } + checkMomentInstance(attr, value); + return value.toJSON(); // Use ISO8601 notation, adjusted to UTC + }, + + dateLib: 'moment' + }, + luxonDate: { + parse: function parse(attr, value) { + if (value === null || value === undefined) { + return null; + } + return luxon.DateTime.fromFormat(value, LUXON_DATE_FORMAT); + }, + toJS: function toJS(attr, value) { + if (value === null || value === undefined) { + return null; + } + checkLuxonDateTime(attr, value); + return value.toFormat(LUXON_DATE_FORMAT); + }, + + dateLib: 'luxon' + }, + luxonDatetime: { + parse: function parse(attr, value) { + if (value === null) { + return null; + } + return luxon.DateTime.fromFormat(value, LUXON_DATETIME_FORMAT); + }, + toJS: function toJS(attr, value) { + if (value === null) { + return null; + } + checkLuxonDateTime(attr, value); + return value.toFormat(LUXON_DATETIME_FORMAT); + }, + + dateLib: 'luxon' + }, + date: { + parse: function parse() { + var _CASTS$; + + return (_CASTS$ = CASTS[DATE_LIB + 'Date']).parse.apply(_CASTS$, arguments); + }, + toJS: function toJS() { + var _CASTS$2; + + return (_CASTS$2 = CASTS[DATE_LIB + 'Date']).toJS.apply(_CASTS$2, arguments); + }, + + get dateLib() { + return DATE_LIB; + } + }, + datetime: { + parse: function parse() { + var _CASTS$3; + + return (_CASTS$3 = CASTS[DATE_LIB + 'Datetime']).parse.apply(_CASTS$3, arguments); + }, + toJS: function toJS() { + var _CASTS$4; + + return (_CASTS$4 = CASTS[DATE_LIB + 'Datetime']).toJS.apply(_CASTS$4, arguments); + }, + + get dateLib() { + return DATE_LIB; + } + }, + enum: function _enum(expectedValues) { + invariant(lodash.isArray(expectedValues), 'Invalid argument suplied to `Casts.enum`, expected an instance of array.'); + function checkExpectedValues(attr, value) { + if (value === null) { + return null; + } + if (expectedValues.includes(value)) { + return value; + } + invariant(false, 'Value set to attribute `' + attr + '`, ' + JSON.stringify(value) + ', is not one of the allowed enum: ' + JSON.stringify(expectedValues)); + } + return { + parse: checkExpectedValues, + toJS: checkExpectedValues + }; + } +}; + +exports.Model = Model; +exports.Store = Store; +exports.BinderApi = BinderApi; +exports.Casts = CASTS; +exports.configureDateLib = configureDateLib; diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js new file mode 100644 index 0000000..a037d8b --- /dev/null +++ b/dist/mobx-spine.es.js @@ -0,0 +1,2330 @@ +import { observable, computed, action, autorun, isObservableProp, extendObservable, isObservableArray, isObservableObject, toJS } from 'mobx'; +import { isArray, map, filter, find, sortBy, forIn, omit, isPlainObject, result, uniqBy, each, mapValues, get, uniqueId, uniq, mapKeys } from 'lodash'; +import axios from 'axios'; +import moment from 'moment'; +import { DateTime } from 'luxon'; + +function invariant(condition) { + var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Illegal state'; + + if (!condition) { + throw new Error('[mobx-spine] ' + message); + } +} + +// lodash's `snakeCase` method removes dots from the string; this breaks mobx-spine +function camelToSnake(s) { + return s.replace(/([A-Z])/g, function ($1) { + return '_' + $1.toLowerCase(); + }); +} + +// lodash's `camelCase` method removes dots from the string; this breaks mobx-spine +function snakeToCamel(s) { + if (s.startsWith('_')) { + return s; + } + return s.replace(/_\w/g, function (m) { + return m[1].toUpperCase(); + }); +} + +// ['kind.breed', 'owner'] => { 'owner': {}, 'kind': {'breed': {}}} +function relationsToNestedKeys(relations) { + var nestedRelations = {}; + + relations.forEach(function (rel) { + var current = nestedRelations; + var components = rel.split('.'); + var len = components.length; + + for (var i = 0; i < len; ++i) { + var head = components[i]; + if (current[head] === undefined) { + current[head] = {}; + } + current = current[head]; + } + }); + + return nestedRelations; +} + +// Use output of relationsToNestedKeys to iterate each relation, fn is called on each model and store. +function forNestedRelations(model, nestedRelations, fn) { + Object.keys(nestedRelations).forEach(function (key) { + if (Object.keys(nestedRelations[key]).length > 0) { + if (model[key].forEach) { + model[key].forEach(function (m) { + forNestedRelations(m, nestedRelations[key], fn); + }); + + fn(model); + } else { + forNestedRelations(model[key], nestedRelations[key], fn); + } + } + + if (model[key].forEach) { + model[key].forEach(fn); + } + + fn(model[key]); + }); +} + +var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +}; + +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; + }; +}(); + +var defineProperty = function (obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; +}; + +var _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; +}; + +var objectWithoutProperties = function (obj, keys) { + var target = {}; + + for (var i in obj) { + if (keys.indexOf(i) >= 0) continue; + if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; + target[i] = obj[i]; + } + + return target; +}; + +var _class, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _class2, _temp; + +function _initDefineProp(target, property, descriptor, context) { + if (!descriptor) return; + Object.defineProperty(target, property, { + enumerable: descriptor.enumerable, + configurable: descriptor.configurable, + writable: descriptor.writable, + value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 + }); +} + +function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { + var desc = {}; + Object['ke' + 'ys'](descriptor).forEach(function (key) { + desc[key] = descriptor[key]; + }); + desc.enumerable = !!desc.enumerable; + desc.configurable = !!desc.configurable; + + if ('value' in desc || desc.initializer) { + desc.writable = true; + } + + desc = decorators.slice().reverse().reduce(function (desc, decorator) { + return decorator(target, property, desc) || desc; + }, desc); + + if (context && desc.initializer !== void 0) { + desc.value = desc.initializer ? desc.initializer.call(context) : void 0; + desc.initializer = undefined; + } + + if (desc.initializer === void 0) { + Object['define' + 'Property'](target, property, desc); + desc = null; + } + + return desc; +} +var AVAILABLE_CONST_OPTIONS = ['relations', 'limit', 'comparator', 'params', 'repository']; + +var Store = (_class = (_temp = _class2 = function () { + createClass(Store, [{ + key: 'url', + value: function url() { + // Try to auto-generate the URL. + var bname = this.constructor.backendResourceName; + if (bname) { + return '/' + bname + '/'; + } + return null; + } + // The set of models has changed + + // Holds the fetch parameters + + }, { + key: 'initialize', + + + // Empty function, but can be overridden if you want to do something after initializing the model. + value: function initialize() {} + }, { + key: 'isLoading', + get: function get$$1() { + return this.__pendingRequestCount > 0; + } + }, { + key: 'length', + get: function get$$1() { + return this.models.length; + } + }, { + key: 'backendResourceName', + set: function set$$1(v) { + invariant(false, '`backendResourceName` should be a static property on the store.'); + } + }]); + + function Store() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + classCallCheck(this, Store); + + _initDefineProp(this, 'models', _descriptor, this); + + _initDefineProp(this, 'params', _descriptor2, this); + + _initDefineProp(this, '__pendingRequestCount', _descriptor3, this); + + _initDefineProp(this, '__setChanged', _descriptor4, this); + + _initDefineProp(this, '__state', _descriptor5, this); + + this.__activeRelations = []; + this.Model = null; + this.api = null; + + invariant(isPlainObject(options), 'Store only accepts an object with options. Chain `.parse(data)` to add models.'); + forIn(options, function (value, option) { + invariant(AVAILABLE_CONST_OPTIONS.includes(option), 'Unknown option passed to store: ' + option); + }); + this.__repository = options.repository; + if (options.relations) { + this.__parseRelations(options.relations); + } + if (options.limit !== undefined) { + this.setLimit(options.limit); + } + if (options.comparator) { + this.comparator = options.comparator; + } + if (options.params) { + this.params = options.params; + } + this.initialize(); + } + + createClass(Store, [{ + key: '__parseRelations', + value: function __parseRelations(activeRelations) { + this.__activeRelations = activeRelations; + } + }, { + key: '__getApi', + value: function __getApi() { + invariant(this.api, 'You are trying to perform a API request without an `api` property defined on the store.'); + invariant(result(this, 'url'), 'You are trying to perform a API request without an `url` property defined on the store.'); + return this.api; + } + }, { + key: 'fromBackend', + value: function fromBackend(_ref) { + var _this = this; + + var data = _ref.data, + repos = _ref.repos, + relMapping = _ref.relMapping, + reverseRelMapping = _ref.reverseRelMapping; + + invariant(data, 'Backend error. Data is not set. HINT: DID YOU FORGET THE M2M again?'); + + this.models.replace(data.map(function (record) { + // TODO: I'm not happy at all about how this looks. + // We'll need to finetune some things, but hey, for now it works. + var model = _this._newModel(); + model.fromBackend({ + data: record, + repos: repos, + relMapping: relMapping, + reverseRelMapping: reverseRelMapping + }); + return model; + })); + this.sort(); + } + }, { + key: '_newModel', + value: function _newModel() { + var model = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + + return new this.Model(model, { + store: this, + relations: this.__activeRelations + }); + } + }, { + key: 'sort', + value: function sort() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + invariant(isPlainObject(options), 'Expecting a plain object for options.'); + if (!this.comparator) { + return this; + } + if (typeof this.comparator === 'string') { + this.models.replace(this.sortBy(this.comparator)); + } else { + this.models.replace(this.models.slice().sort(this.comparator)); + } + return this; + } + }, { + key: 'parse', + value: function parse(models) { + invariant(isArray(models), 'Parameter supplied to `parse()` is not an array, got: ' + JSON.stringify(models)); + // Parse does not mutate __setChanged, as it is used in + // fromBackend in the model... + this.models.replace(models.map(this._newModel.bind(this))); + this.sort(); + + return this; + } + }, { + key: 'parseValidationErrors', + value: function parseValidationErrors(valErrors) { + this.each(function (model) { + model.parseValidationErrors(valErrors); + }); + } + }, { + key: 'clearValidationErrors', + value: function clearValidationErrors() { + this.each(function (model) { + model.clearValidationErrors(); + }); + } + }, { + key: 'add', + value: function add(models) { + var _this2 = this; + + var singular = !isArray(models); + models = singular ? [models] : models.slice(); + + var modelInstances = models.map(this._newModel.bind(this)); + + modelInstances.forEach(function (modelInstance) { + var primaryValue = modelInstance[_this2.Model.primaryKey]; + invariant(!primaryValue || !_this2.get(primaryValue), 'A model with the same primary key value "' + primaryValue + '" already exists in this store.'); + _this2.__setChanged = true; + _this2.models.push(modelInstance); + }); + this.sort(); + + return singular ? modelInstances[0] : modelInstances; + } + }, { + key: 'remove', + value: function remove(models) { + var _this3 = this; + + var singular = !isArray(models); + models = singular ? [models] : models.slice(); + + models.forEach(function (model) { + return _this3.models.remove(model); + }); + if (models.length > 0) { + this.__setChanged = true; + } + return models; + } + }, { + key: 'removeById', + value: function removeById(ids) { + var _this4 = this; + + var singular = !isArray(ids); + ids = singular ? [ids] : ids.slice(); + invariant(!ids.some(isNaN), 'Cannot remove a model by id that is not a number: ' + JSON.stringify(ids)); + + var models = ids.map(function (id) { + return _this4.get(id); + }); + + models.forEach(function (model) { + if (model) { + _this4.models.remove(model); + _this4.__setChanged = true; + } + }); + + return models; + } + }, { + key: 'clear', + value: function clear() { + var length = this.models.length; + this.models.clear(); + + if (length > 0) { + this.__setChanged = true; + } + } + }, { + key: 'buildFetchData', + value: function buildFetchData(options) { + return Object.assign(this.__getApi().buildFetchStoreParams(this), this.params, options.data); + } + }, { + key: 'fetch', + value: function fetch() { + var _this5 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + + var data = this.buildFetchData(options); + var promise = this.wrapPendingRequestCount(this.__getApi().fetchStore({ + url: options.url || result(this, 'url'), + data: data, + requestOptions: omit(options, 'data') + }).then(action(function (res) { + _this5.__state.totalRecords = res.totalRecords; + _this5.fromBackend(res); + + return res.response; + }))); + + return promise; + } + }, { + key: '__parseNewIds', + value: function __parseNewIds(idMaps) { + this.each(function (model) { + return model.__parseNewIds(idMaps); + }); + } + }, { + key: 'toJS', + value: function toJS$$1() { + return this.models.map(function (model) { + return model.toJS(); + }); + } + + // Methods for pagination. + + }, { + key: 'getPageOffset', + value: function getPageOffset() { + return (this.__state.currentPage - 1) * this.__state.limit; + } + }, { + key: 'setLimit', + value: function setLimit(limit) { + invariant(!limit || Number.isInteger(limit), 'Page limit should be a number or falsy value.'); + this.__state.limit = limit || null; + } + }, { + key: 'getNextPage', + value: function getNextPage() { + invariant(this.hasNextPage, 'There is no next page.'); + this.__state.currentPage += 1; + return this.fetch(); + } + }, { + key: 'getPreviousPage', + value: function getPreviousPage() { + invariant(this.hasPreviousPage, 'There is no previous page.'); + this.__state.currentPage -= 1; + return this.fetch(); + } + }, { + key: 'setPage', + value: function setPage() { + var page = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + invariant(Number.isInteger(page) && page >= 1, 'Page should be a number above 1.'); + this.__state.currentPage = page; + if (options.fetch === undefined || options.fetch) { + return this.fetch(); + } + invariant( + // Always allow to go to page 1. + page <= (this.totalPages || 1), 'Page should be between 1 and ' + this.totalPages + '.'); + return Promise.resolve(); + } + }, { + key: 'clearSetChanges', + value: function clearSetChanges() { + this.__setChanged = false; + } + }, { + key: 'toBackendAll', + value: function toBackendAll() { + var _this6 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var relevantModels = options.onlyChanges ? this.models.filter(function (model) { + return model.isNew || model.hasUserChanges; + }) : this.models; + var modelData = relevantModels.map(function (model) { + return model.toBackendAll(options); + }); + + var data = []; + var relations = {}; + + modelData.forEach(function (model) { + data = data.concat(model.data); + forIn(model.relations, function (relModel, key) { + relations[key] = relations[key] ? relations[key].concat(relModel) : relModel; + // TODO: this primaryKey is not the primaryKey of the relation we're de-duplicating... + relations[key] = uniqBy(relations[key], _this6.Model.primaryKey); + }); + }); + + return { data: data, relations: relations }; + } + + // Create a new instance of this store with a predicate applied. + // This new store will be automatically kept in-sync with all models that adhere to the predicate. + + }, { + key: 'virtualStore', + value: function virtualStore(_ref2) { + var _this7 = this; + + var filter$$1 = _ref2.filter, + comparator = _ref2.comparator; + + var store = new this.constructor({ + relations: this.__activeRelations, + comparator: comparator + }); + + // Oh gawd MobX is so awesome. + var events = autorun(function () { + var models = _this7.filter(filter$$1); + store.models.replace(models); + store.sort(); + + // When the parent store is busy, make sure the virtual store is + // also busy. + store.__pendingRequestCount = _this7.__pendingRequestCount; + }); + + store.unsubscribeVirtualStore = events; + + return store; + } + + // Helper methods to read models. + + }, { + key: 'get', + value: function get$$1(id) { + // The id can be defined as a string or int, but we want it to work in both cases. + return this.models.find(function (model) { + return model[model.constructor.primaryKey] == id; + } // eslint-disable-line eqeqeq + ); + } + }, { + key: 'getByIds', + value: function getByIds(ids) { + return this.models.filter(function (model) { + var id = model[model.constructor.primaryKey]; + return ids.includes(id) || ids.includes('' + id); + }); + } + }, { + key: 'map', + value: function map$$1(predicate) { + return map(this.models, predicate); + } + }, { + key: 'mapByPrimaryKey', + value: function mapByPrimaryKey() { + return this.map(this.Model.primaryKey); + } + }, { + key: 'filter', + value: function filter$$1(predicate) { + return filter(this.models, predicate); + } + }, { + key: 'find', + value: function find$$1(predicate) { + return find(this.models, predicate); + } + }, { + key: 'each', + value: function each$$1(predicate) { + return this.models.forEach(predicate); + } + }, { + key: 'forEach', + value: function forEach(predicate) { + return this.models.forEach(predicate); + } + }, { + key: 'sortBy', + value: function sortBy$$1(iteratees) { + return sortBy(this.models, iteratees); + } + }, { + key: 'at', + value: function at(index) { + var zeroLength = this.length - 1; + invariant(index <= zeroLength, 'Index ' + index + ' is out of bounds (max ' + zeroLength + ').'); + if (index < 0) { + index += this.length; + } + return this.models[index]; + } + }, { + key: 'wrapPendingRequestCount', + value: function wrapPendingRequestCount(promise) { + var _this8 = this; + + this.__pendingRequestCount++; + + return promise.then(function (res) { + _this8.__pendingRequestCount--; + return res; + }).catch(function (err) { + _this8.__pendingRequestCount--; + throw err; + }); + } + }, { + key: 'saveAllFiles', + value: function saveAllFiles() { + var relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var promises = []; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = this.models[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var model = _step.value; + + promises.push(model.saveAllFiles(relations)); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return Promise.all(promises); + } + }, { + key: 'totalPages', + get: function get$$1() { + if (!this.__state.limit) { + return 0; + } + return Math.ceil(this.__state.totalRecords / this.__state.limit); + } + }, { + key: 'currentPage', + get: function get$$1() { + return this.__state.currentPage; + } + }, { + key: 'hasNextPage', + get: function get$$1() { + return this.__state.currentPage + 1 <= this.totalPages; + } + }, { + key: 'hasPreviousPage', + get: function get$$1() { + return this.__state.currentPage > 1; + } + }, { + key: 'hasUserChanges', + get: function get$$1() { + return this.hasSetChanges || this.models.some(function (m) { + return m.hasUserChanges; + }); + } + + // TODO: Maybe we can keep track of what got added and what got + // removed exactly. For now this should be enough. + + }, { + key: 'hasSetChanges', + get: function get$$1() { + return this.__setChanged; + } + }]); + return Store; +}(), _class2.backendResourceName = '', _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, 'models', [observable], { + enumerable: true, + initializer: function initializer() { + return []; + } +}), _descriptor2 = _applyDecoratedDescriptor(_class.prototype, 'params', [observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor3 = _applyDecoratedDescriptor(_class.prototype, '__pendingRequestCount', [observable], { + enumerable: true, + initializer: function initializer() { + return 0; + } +}), _descriptor4 = _applyDecoratedDescriptor(_class.prototype, '__setChanged', [observable], { + enumerable: true, + initializer: function initializer() { + return false; + } +}), _descriptor5 = _applyDecoratedDescriptor(_class.prototype, '__state', [observable], { + enumerable: true, + initializer: function initializer() { + return { + currentPage: 1, + limit: 25, + totalRecords: 0 + }; + } +}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype)), _class); + +var _class$1, _descriptor$1, _descriptor2$1, _descriptor3$1, _descriptor4$1, _descriptor5$1, _descriptor6, _descriptor7, _class2$1, _temp$1; + +function _initDefineProp$1(target, property, descriptor, context) { + if (!descriptor) return; + Object.defineProperty(target, property, { + enumerable: descriptor.enumerable, + configurable: descriptor.configurable, + writable: descriptor.writable, + value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 + }); +} + +function _applyDecoratedDescriptor$1(target, property, decorators, descriptor, context) { + var desc = {}; + Object['ke' + 'ys'](descriptor).forEach(function (key) { + desc[key] = descriptor[key]; + }); + desc.enumerable = !!desc.enumerable; + desc.configurable = !!desc.configurable; + + if ('value' in desc || desc.initializer) { + desc.writable = true; + } + + desc = decorators.slice().reverse().reduce(function (desc, decorator) { + return decorator(target, property, desc) || desc; + }, desc); + + if (context && desc.initializer !== void 0) { + desc.value = desc.initializer ? desc.initializer.call(context) : void 0; + desc.initializer = undefined; + } + + if (desc.initializer === void 0) { + Object['define' + 'Property'](target, property, desc); + desc = null; + } + + return desc; +} + +function concatInDict(dict, key, value) { + dict[key] = dict[key] ? dict[key].concat(value) : value; +} + +// Find the relation name before the first dot, and include all other relations after it +// Example: input `animal.kind.breed` output -> `['animal', 'kind.breed']` +var RE_SPLIT_FIRST_RELATION = /([^.]+)\.(.+)/; + +// TODO: find a way to get a list of existing properties automatically. +var FORBIDDEN_ATTRS = ['url', 'urlRoot', 'api', 'isNew', 'isLoading', 'parse', 'save', 'clear']; + +var Model = (_class$1 = (_temp$1 = _class2$1 = function () { + createClass(Model, [{ + key: 'urlRoot', + + // How the model is known at the backend. This is useful when the model is in a relation that has a different name. + value: function urlRoot() { + // Try to auto-generate the URL. + var bname = this.constructor.backendResourceName; + if (bname) { + return '/' + bname + '/'; + } + return null; + } + // Holds original attributes with values, so `clear()` knows what to reset to (quite ugly). + + // Holds activated - nested - relations (e.g. `['animal', 'animal.breed']`) + + // Holds activated - non-nested - relations (e.g. `['animal']`) + + // A `cid` can be used to identify the model locally. + + // URL query params that are added to fetch requests. + + // Holds fields (attrs+relations) that have been changed via setInput() + + + // File state + + }, { + key: 'wrapPendingRequestCount', + value: function wrapPendingRequestCount(promise) { + var _this = this; + + this.__pendingRequestCount++; + + return promise.then(function (res) { + _this.__pendingRequestCount--; + return res; + }).catch(function (err) { + _this.__pendingRequestCount--; + throw err; + }); + } + + // Useful to reference to this model in a relation - that is not yet saved to the backend. + + }, { + key: 'getNegativeId', + value: function getNegativeId() { + return -parseInt(this.cid.replace('m', '')); + } + }, { + key: 'getInternalId', + value: function getInternalId() { + if (this.isNew) { + return this.getNegativeId(); + } + return this[this.constructor.primaryKey]; + } + }, { + key: 'casts', + value: function casts() { + return {}; + } + }, { + key: 'fileFields', + value: function fileFields() { + return this.constructor.fileFields; + } + }, { + key: 'pickFields', + value: function pickFields() { + return this.constructor.pickFields; + } + }, { + key: 'omitFields', + value: function omitFields() { + return this.constructor.omitFields; + } + + // Empty function, but can be overridden if you want to do something after initializing the model. + + }, { + key: 'initialize', + value: function initialize() {} + }, { + key: 'url', + get: function get$$1() { + var id = this[this.constructor.primaryKey]; + return '' + result(this, 'urlRoot') + (id ? id + '/' : ''); + } + }, { + key: 'isNew', + get: function get$$1() { + return !this[this.constructor.primaryKey]; + } + }, { + key: 'isLoading', + get: function get$$1() { + return this.__pendingRequestCount > 0; + } + }, { + key: 'primaryKey', + set: function set$$1(v) { + invariant(false, '`primaryKey` should be a static property on the model.'); + } + }, { + key: 'backendResourceName', + set: function set$$1(v) { + invariant(false, '`backendResourceName` should be a static property on the model.'); + } + }]); + + function Model(data) { + var _this2 = this; + + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + classCallCheck(this, Model); + this.__attributes = []; + this.__originalAttributes = {}; + this.__activeRelations = []; + this.__activeCurrentRelations = []; + this.api = null; + this.cid = 'm' + uniqueId(); + + _initDefineProp$1(this, '__backendValidationErrors', _descriptor$1, this); + + _initDefineProp$1(this, '__pendingRequestCount', _descriptor2$1, this); + + _initDefineProp$1(this, '__fetchParams', _descriptor3$1, this); + + _initDefineProp$1(this, '__changes', _descriptor4$1, this); + + _initDefineProp$1(this, '__fileChanges', _descriptor5$1, this); + + _initDefineProp$1(this, '__fileDeletions', _descriptor6, this); + + _initDefineProp$1(this, '__fileExists', _descriptor7, this); + + this.__store = options.store; + this.__repository = options.repository; + // Find all attributes. Not all observables are an attribute. + forIn(this, function (value, key) { + if (!key.startsWith('__') && isObservableProp(_this2, key)) { + invariant(!FORBIDDEN_ATTRS.includes(key), 'Forbidden attribute key used: `' + key + '`'); + _this2.__attributes.push(key); + var newValue = value; + // An array or object observable can be mutated, so we want to ensure we always have + // the original not-yet-mutated object/array. + if (isObservableArray(value)) { + newValue = value.slice(); + } else if (isObservableObject(value)) { + newValue = Object.assign({}, value); + } + _this2.__originalAttributes[key] = newValue; + } + }); + if (options.relations) { + this.__parseRelations(options.relations); + } + if (data) { + this.parse(data); + } + this.initialize(); + + this.saveFile = this.saveFile.bind(this); + } + + createClass(Model, [{ + key: '__parseRelations', + value: function __parseRelations(activeRelations) { + var _this3 = this; + + this.__activeRelations = activeRelations; + // TODO: No idea why getting the relations only works when it's a Function. + var relations = this.relations && this.relations(); + var relModels = {}; + activeRelations.forEach(function (aRel) { + // If aRel is null, this relation is already defined by another aRel + // IE.: town.restaurants.chef && town + if (aRel === null || !!_this3[aRel]) { + return; + } + var relNames = aRel.match(RE_SPLIT_FIRST_RELATION); + + var currentRel = relNames ? relNames[1] : aRel; + var otherRelNames = relNames && relNames[2]; + var currentProp = relModels[currentRel]; + var otherRels = otherRelNames && [otherRelNames]; + + // When two nested relations are defined next to each other (e.g. `['kind.breed', 'kind.location']`), + // the relation `kind` only needs to be initialized once. + relModels[currentRel] = currentProp ? currentProp.concat(otherRels) : otherRels; + invariant(!_this3.__attributes.includes(currentRel), 'Cannot define `' + currentRel + '` as both an attribute and a relation. You probably need to remove the attribute.'); + if (!_this3.__activeCurrentRelations.includes(currentRel)) { + _this3.__activeCurrentRelations.push(currentRel); + } + }); + // extendObservable where we omit the fields that are already created from other relations + extendObservable(this, mapValues(omit(relModels, Object.keys(relModels).filter(function (rel) { + return !!_this3[rel]; + })), function (otherRelNames, relName) { + var RelModel = relations[relName]; + invariant(RelModel, 'Specified relation "' + relName + '" does not exist on model.'); + var options = { relations: otherRelNames }; + if (RelModel.prototype instanceof Store) { + return new RelModel(options); + } + return new RelModel(null, options); + })); + } + + // Many backends use snake_case for attribute names, so we convert to snake_case by default. + + }, { + key: 'clearUserFieldChanges', + value: function clearUserFieldChanges() { + this.__changes.clear(); + } + }, { + key: 'clearUserFileChanges', + value: function clearUserFileChanges() { + this.__fileChanges = {}; + this.__fileDeletions = {}; + this.__fileExists = {}; + } + }, { + key: 'clearUserChanges', + value: function clearUserChanges() { + this.clearUserFieldChanges(); + this.clearUserFileChanges(); + } + }, { + key: 'toBackend', + value: function toBackend() { + var _this4 = this; + + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var _ref$data = _ref.data, + data = _ref$data === undefined ? {} : _ref$data, + _ref$mapData = _ref.mapData, + mapData = _ref$mapData === undefined ? function (x) { + return x; + } : _ref$mapData, + options = objectWithoutProperties(_ref, ['data', 'mapData']); + + var output = {}; + // By default we'll include all fields (attributes+relations), but sometimes you might want to specify the fields to be included. + var fieldFilter = function fieldFilter(field) { + if (!_this4.fieldFilter(field)) { + return false; + } + if (options.fields) { + return options.fields.includes(field); + } + if (!_this4.isNew && options.onlyChanges) { + var forceFields = options.forceFields || []; + return forceFields.includes(field) || _this4.__changes.includes(field) || _this4[field] instanceof Store && _this4[field].hasSetChanges || + // isNew is always true for relations that haven't been saved. + // If no property has been tweaked, its id serializes as null. + // So, we need to skip saving the id if new and no changes. + _this4[field] instanceof Model && _this4[field].isNew && _this4[field].hasUserChanges; + } + return true; + }; + this.__attributes.filter(fieldFilter).forEach(function (attr) { + if (!attr.startsWith('_')) { + output[_this4.constructor.toBackendAttrKey(attr)] = _this4.__toJSAttr(attr, _this4[attr]); + } + }); + + // Primary key is always forced to be included. + output[this.constructor.primaryKey] = this[this.constructor.primaryKey]; + + // Add active relations as id. + this.__activeCurrentRelations.filter(fieldFilter).forEach(function (currentRel) { + var rel = _this4[currentRel]; + var relBackendName = _this4.constructor.toBackendAttrKey(currentRel); + if (rel instanceof Model) { + output[relBackendName] = rel[rel.constructor.primaryKey]; + } + if (rel instanceof Store) { + output[relBackendName] = rel.mapByPrimaryKey(); + } + }); + + Object.assign(output, data); + return mapData(output); + } + }, { + key: 'toBackendAll', + value: function toBackendAll() { + var _this5 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var nestedRelations = options.nestedRelations || {}; + var data = this.toBackend({ + data: options.data, + mapData: options.mapData, + onlyChanges: options.onlyChanges + }); + + if (data[this.constructor.primaryKey] === null) { + data[this.constructor.primaryKey] = this.getNegativeId(); + } + + var relations = {}; + + this.__activeCurrentRelations.forEach(function (currentRel) { + var rel = _this5[currentRel]; + var relBackendName = _this5.constructor.toBackendAttrKey(currentRel); + var subRelations = nestedRelations[currentRel]; + + if (subRelations !== undefined) { + if (data[relBackendName] === null) { + data[relBackendName] = rel.getNegativeId(); + } else if (isArray(data[relBackendName])) { + data[relBackendName] = uniq(data[relBackendName].map(function (pk, i) { + return pk === null ? rel.at(i).getNegativeId() : pk; + })); + } else if (options.onlyChanges && !rel.hasUserChanges) { + return; + } + + var relBackendData = rel.toBackendAll({ + nestedRelations: subRelations, + onlyChanges: options.onlyChanges + }); + + // Sometimes the backend knows the relation by a different name, e.g. the relation is called + // `activities`, but the name in the backend is `activity`. + // In that case, you can add `static backendResourceName = 'activity';` to that model. + var realBackendName = rel.constructor.backendResourceName || relBackendName; + + if (relBackendData.data.length > 0) { + concatInDict(relations, realBackendName, relBackendData.data); + + // De-duplicate relations based on `primaryKey`. + // TODO: Avoid serializing recursively multiple times in the first place? + // TODO: What if different relations have different "freshness"? + relations[realBackendName] = uniqBy(relations[realBackendName], rel.constructor.primaryKey || rel.Model.primaryKey); + } + + // There could still be changes in nested relations, + // include those anyway! + forIn(relBackendData.relations, function (relB, key) { + concatInDict(relations, key, relB); + }); + } + }); + + return { data: [data], relations: relations }; + } + + /** + * Makes this model a copy of the specified model + * or returns a copy of the current model when no model to copy is given + * It also clones the changes that were in the specified model. + * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. + * Cloning + * + * @param source {Model} - The model that should be copied + * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} + */ + + }, { + key: 'copy', + value: function copy() { + var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; + + var copiedModel = void 0; + // If our source is not a model it is 'probably' the options + if (source !== undefined && !(source instanceof Model)) { + options = source; + source = undefined; + } + + // Make sure that we have the correct model + if (source === undefined) { + source = this; + copiedModel = new source.constructor({ relations: source.__activeRelations }); + } else if (this.constructor !== source.constructor) { + copiedModel = new source.constructor({ relations: source.__activeRelations }); + } else { + copiedModel = this; + } + + var copyChanges = options.copyChanges; + + // Maintain the relations after copy + // this.__activeRelations = source.__activeRelations; + copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; + + copiedModel.__parseRelations(source.__activeRelations); + // Copy all fields and values from the specified model + copiedModel.parse(source.toJS()); + + // Set only the changed attributes + if (copyChanges) { + copiedModel._copyChanges(source); + } + + return copiedModel; + } + + /** + * Goes over model and all related models to set the changed values and notify the store + * + * @param source - the model to copy + * @param store - the store of the current model, to setChanged if there are changes + * @private + */ + + }, { + key: '_copyChanges', + value: function _copyChanges(source, store) { + var _this6 = this; + + // Maintain the relations after copy + this.__activeRelations = source.__activeRelations; + this.__activeCurrentRelations = source.__activeCurrentRelations; + + // Copy all changed fields and notify the store that there are changes + if (source.__changes.length > 0) { + if (store) { + store.__setChanged = true; + } else if (this.__store) { + this.__store.__setChanged = true; + } + + source.__changes.forEach(function (changedAttribute) { + _this6.setInput(changedAttribute, source[changedAttribute]); + }); + } + // Undefined safety + if (source.__activeCurrentRelations.length > 0) { + // Set the changes for all related models with changes + source.__activeCurrentRelations.forEach(function (relation) { + if (relation && source[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); + }); + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); + } + } + } + }); + } + } + }, { + key: 'toJS', + value: function toJS$$1() { + var _this7 = this; + + var output = {}; + this.__attributes.forEach(function (attr) { + output[attr] = _this7.__toJSAttr(attr, _this7[attr]); + }); + + this.__activeCurrentRelations.forEach(function (currentRel) { + var model = _this7[currentRel]; + if (model) { + output[currentRel] = model.toJS(); + } + }); + return output; + } + }, { + key: '__toJSAttr', + value: function __toJSAttr(attr, value) { + var casts = this.casts(); + var cast = casts[attr]; + if (cast !== undefined) { + return toJS(cast.toJS(attr, value)); + } + return toJS(value); + } + }, { + key: 'setFetchParams', + value: function setFetchParams(params) { + this.__fetchParams = Object.assign({}, params); + } + }, { + key: '__parseRepositoryToData', + value: function __parseRepositoryToData(key, repository) { + if (isArray(key)) { + return filter(repository, function (m) { + return key.includes(m.id); + }); + } + return find(repository, { id: key }); + } + }, { + key: '__parseReverseRepositoryToData', + value: function __parseReverseRepositoryToData(reverseKeyName, key, repository) { + var searchKey = {}; + searchKey[reverseKeyName] = key; + return filter(repository, searchKey); + } + + /** + * We handle the fromBackend recursively. + * But when recursing, we don't send the full repository, we need to only send the repo + * relevant to the relation. + * + * So when we have a customer with a town.restaurants relation, + * we get a "town.restaurants": "restaurant", relMapping from Binder + * + * Here we create a scoped repository. + * The root gets a `town.restaurants` repo, but the `town` relation only gets the `restaurants` repo + */ + + }, { + key: '__scopeBackendResponse', + value: function __scopeBackendResponse(_ref2) { + var _this8 = this; + + var data = _ref2.data, + targetRelName = _ref2.targetRelName, + repos = _ref2.repos, + mapping = _ref2.mapping, + reverseMapping = _ref2.reverseMapping; + + var scopedData = null; + var relevant = false; + var scopedRepos = {}; + var scopedRelMapping = {}; + var scopedReverseRelMapping = {}; + + if (!data) { + return null; + } + + forIn(mapping, function (repoName, backendRelName) { + var repository = repos[repoName]; + // For backwards compatibility, reverseMapping is optional (for now) + var reverseRelName = reverseMapping ? reverseMapping[backendRelName] : null; + var relName = _this8.constructor.fromBackendAttrKey(backendRelName); + + if (targetRelName === relName) { + var relKey = data[_this8.constructor.toBackendAttrKey(relName)]; + if (relKey !== undefined) { + relevant = true; + scopedData = _this8.__parseRepositoryToData(relKey, repository); + } else if (repository && reverseRelName) { + var pk = data[_this8.constructor.primaryKey]; + relevant = true; + scopedData = _this8.__parseReverseRepositoryToData(reverseRelName, pk, repository); + if (_this8.relations(relName).prototype instanceof Model) { + if (scopedData.length === 0) { + scopedData = null; + } else if (scopedData.length === 1) { + scopedData = scopedData[0]; + } else { + throw new Error('multiple models found for related model'); + } + } + } + return; + } + + if (relName.startsWith(targetRelName + '.')) { + // If we have town.restaurants and the targetRel = town + // we need "restaurants" in the repository + relevant = true; + var backendRelNames = backendRelName.match(RE_SPLIT_FIRST_RELATION); + var scopedBackendRelName = backendRelNames[2]; + scopedRepos[repoName] = repository; + scopedRelMapping[scopedBackendRelName] = repoName; + scopedReverseRelMapping[scopedBackendRelName] = reverseMapping ? reverseMapping[backendRelName] : null; + } + }); + + if (!relevant) { + return null; + } + + return { scopedData: scopedData, scopedRepos: scopedRepos, scopedRelMapping: scopedRelMapping, scopedReverseRelMapping: scopedReverseRelMapping }; + } + + // `data` contains properties for the current model. + // `repos` is an object of "repositories". A repository is + // e.g. "animal_kind", while the relation name would be "kind". + // `relMapping` maps relation names to repositories. + + }, { + key: 'fromBackend', + value: function fromBackend(_ref3) { + var _this9 = this; + + var data = _ref3.data, + repos = _ref3.repos, + relMapping = _ref3.relMapping, + reverseRelMapping = _ref3.reverseRelMapping; + + // We handle the fromBackend recursively. On each relation of the source model + // fromBackend gets called as well, but with data scoped for itself + // + // So when we have a model with a `town.restaurants.chef` relation, + // we call fromBackend on the `town` relation. + each(this.__activeCurrentRelations, function (relName) { + var rel = _this9[relName]; + var resScoped = _this9.__scopeBackendResponse({ + data: data, + targetRelName: relName, + repos: repos, + mapping: relMapping, + reverseMapping: reverseRelMapping + }); + + // Make sure we don't parse every relation for nothing + if (!resScoped) { + return; + } + + var scopedData = resScoped.scopedData, + scopedRepos = resScoped.scopedRepos, + scopedRelMapping = resScoped.scopedRelMapping, + scopedReverseRelMapping = resScoped.scopedReverseRelMapping; + + rel.fromBackend({ + data: scopedData, + repos: scopedRepos, + relMapping: scopedRelMapping, + reverseRelMapping: scopedReverseRelMapping + }); + }); + + // Now all repositories are set on the relations, start parsing the actual data. + // `parse()` will recursively fill in all relations. + if (data) { + this.parse(data); + } + } + }, { + key: '__getApi', + value: function __getApi() { + invariant(this.api, 'You are trying to perform a API request without an `api` property defined on the model.'); + invariant(result(this, 'urlRoot'), 'You are trying to perform a API request without an `urlRoot` property defined on the model.'); + return this.api; + } + }, { + key: 'parse', + value: function parse(data) { + var _this10 = this; + + invariant(isPlainObject(data), 'Parameter supplied to `parse()` is not an object, got: ' + JSON.stringify(data)); + + forIn(data, function (value, key) { + var attr = _this10.constructor.fromBackendAttrKey(key); + if (_this10.__attributes.includes(attr)) { + _this10[attr] = _this10.__parseAttr(attr, value); + } else if (_this10.__activeCurrentRelations.includes(attr)) { + // In Binder, a relation property is an `int` or `[int]`, referring to its ID. + // However, it can also be an object if there are nested relations (non flattened). + if (isPlainObject(value) || isPlainObject(get(value, '[0]'))) { + _this10[attr].parse(value); + } else if (value === null) { + // The relation is cleared. + _this10[attr].clear(); + } + } + }); + + return this; + } + }, { + key: '__parseAttr', + value: function __parseAttr(attr, value) { + var casts = this.casts(); + var cast = casts[attr]; + if (cast !== undefined) { + return cast.parse(attr, value); + } + return value; + } + }, { + key: 'saveFile', + value: function saveFile(name) { + var _this11 = this; + + var snakeName = camelToSnake(name); + + if (this.__fileChanges[name]) { + var file = this.__fileChanges[name]; + + var data = new FormData(); + data.append(name, file, file.name); + + return this.api.post('' + this.url + snakeName + '/', data, { headers: { 'Content-Type': 'multipart/form-data' } }).then(action(function (res) { + _this11.__fileExists[name] = true; + delete _this11.__fileChanges[name]; + _this11.saveFromBackend(res); + })); + } else if (this.__fileDeletions[name]) { + if (this.__fileExists[name]) { + return this.api.delete('' + this.url + snakeName + '/').then(action(function () { + _this11.__fileExists[name] = false; + delete _this11.__fileDeletions[name]; + _this11.saveFromBackend({ data: defineProperty({}, snakeName, null) }); + })); + } else { + delete this.__fileDeletions[name]; + } + } else { + return Promise.resolve(); + } + } + }, { + key: 'saveFiles', + value: function saveFiles() { + return Promise.all(this.fileFields().filter(this.fieldFilter).map(this.saveFile)); + } + + /** + * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal save request, example: {onlyChanges: true} + */ + + }, { + key: 'validate', + value: function validate() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate parameter + if (options.params) { + options.params.validate = true; + } else { + options.params = { validate: true }; + } + return this.save(options); + } + }, { + key: 'save', + value: function save() { + var _this12 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.clearValidationErrors(); + return this.wrapPendingRequestCount(this.__getApi().saveModel({ + url: options.url || this.url, + data: this.toBackend({ + data: options.data, + mapData: options.mapData, + fields: options.fields, + onlyChanges: options.onlyChanges + }), + isNew: this.isNew, + requestOptions: omit(options, 'url', 'data', 'mapData') + }).then(action(function (res) { + // Only update the model when we are actually trying to save + if (!options.params || !options.params.validate) { + _this12.saveFromBackend(_extends({}, res, { + data: omit(res.data, _this12.fileFields().map(camelToSnake)) + })); + _this12.clearUserFieldChanges(); + return _this12.saveFiles().then(function () { + _this12.clearUserFileChanges(); + return Promise.resolve(res); + }); + } + })).catch(action(function (err) { + if (err.valErrors) { + _this12.parseValidationErrors(err.valErrors); + } + throw err; + }))); + } + }, { + key: 'setInput', + value: function setInput(name, value) { + invariant(this.__attributes.includes(name) || this.__activeCurrentRelations.includes(name), 'Field `' + name + '` does not exist on the model.'); + if (this.fileFields().includes(name)) { + if (this.__fileExists[name] === undefined) { + this.__fileExists[name] = this[name] !== null; + } + if (value) { + this.__fileChanges[name] = value; + delete this.__fileDeletions[name]; + + value = URL.createObjectURL(value) + '?content_type=' + value.type; + } else { + if (!this.__fileChanges[name] || this.__fileChanges[name].existed) { + this.__fileDeletions[name] = true; + } + delete this.__fileChanges[name]; + + value = null; + } + } + if (!this.__changes.includes(name)) { + this.__changes.push(name); + } + if (this.__activeCurrentRelations.includes(name)) { + if (isArray(value)) { + this[name].clear(); + this[name].add(value.map(function (v) { + return v.toJS(); + })); + } else if (value) { + this[name].parse(value.toJS()); + } else { + this[name].clear(); + } + } else { + this[name] = value; + } + if (this.backendValidationErrors[name]) { + this.__backendValidationErrors = Object.assign(this.backendValidationErrors, defineProperty({}, name, undefined)); + } + } + }, { + key: 'saveAllFiles', + value: function saveAllFiles() { + var relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var promises = [this.saveFiles()]; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = Object.keys(relations)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var rel = _step.value; + + promises.push(this[rel].saveAllFiles(relations[rel])); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return Promise.all(promises); + } + + /** + * Validates a model and relations by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} + */ + + }, { + key: 'validateAll', + value: function validateAll() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate option + if (options.params) { + options.params.validate = true; + } else { + options.params = { validate: true }; + } + return this.saveAll(options); + } + }, { + key: 'saveAll', + value: function saveAll() { + var _this13 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.clearValidationErrors(); + return this.wrapPendingRequestCount(this.__getApi().saveAllModels({ + url: result(this, 'urlRoot'), + model: this, + data: this.toBackendAll({ + data: options.data, + mapData: options.mapData, + nestedRelations: relationsToNestedKeys(options.relations || []), + onlyChanges: options.onlyChanges + }), + requestOptions: omit(options, 'relations', 'data', 'mapData') + }).then(action(function (res) { + // Only update the models if we are actually trying to save + if (!options.params || !options.params.validate) { + _this13.saveFromBackend(res); + _this13.clearUserFieldChanges(); + + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); + } + }); + + return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + _this13.clearUserFileChanges(); + + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFileChanges(); + } + }); + + return res; + }); + } + })).catch(action(function (err) { + if (err.valErrors) { + _this13.parseValidationErrors(err.valErrors); + } + throw err; + }))); + } + + // After saving a model, we should get back an ID mapping from the backend which looks like: + // `{ "animal": [[-1, 10]] }` + + }, { + key: '__parseNewIds', + value: function __parseNewIds(idMaps) { + var _this14 = this; + + var bName = this.constructor.backendResourceName; + if (bName && idMaps[bName]) { + var idMap = idMaps[bName].find(function (ids) { + return ids[0] === _this14.getInternalId(); + }); + if (idMap) { + this[this.constructor.primaryKey] = idMap[1]; + } + } + each(this.__activeCurrentRelations, function (relName) { + var rel = _this14[relName]; + rel.__parseNewIds(idMaps); + }); + } + }, { + key: 'validationErrorFormatter', + value: function validationErrorFormatter(obj) { + return obj.code; + } + }, { + key: 'parseValidationErrors', + value: function parseValidationErrors(valErrors) { + var _this15 = this; + + var bname = this.constructor.backendResourceName; + + if (valErrors[bname]) { + var id = this.getInternalId(); + // When there is no id or negative id, the backend may use the string 'null'. Bit weird, but eh. + var errorsForModel = valErrors[bname][id] || valErrors[bname]['null']; + if (errorsForModel) { + var camelCasedErrors = mapKeys(errorsForModel, function (value, key) { + return snakeToCamel(key); + }); + var formattedErrors = mapValues(camelCasedErrors, function (valError) { + return valError.map(_this15.validationErrorFormatter); + }); + this.__backendValidationErrors = formattedErrors; + } + } + + this.__activeCurrentRelations.forEach(function (currentRel) { + _this15[currentRel].parseValidationErrors(valErrors); + }); + } + }, { + key: 'clearValidationErrors', + value: function clearValidationErrors() { + var _this16 = this; + + this.__backendValidationErrors = {}; + this.__activeCurrentRelations.forEach(function (currentRel) { + _this16[currentRel].clearValidationErrors(); + }); + } + + // This is just a pass-through to make it easier to override parsing backend responses from the backend. + // Sometimes the backend won't return the model after a save because e.g. it is created async. + + }, { + key: 'saveFromBackend', + value: function saveFromBackend(res) { + return this.fromBackend(res); + } + + // TODO: This is a bit hacky... + + }, { + key: 'delete', + value: function _delete() { + var _this17 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var removeFromStore = function removeFromStore() { + return _this17.__store ? _this17.__store.remove(_this17) : null; + }; + if (options.immediate || this.isNew) { + removeFromStore(); + } + if (this.isNew) { + return Promise.resolve(); + } + + return this.wrapPendingRequestCount(this.__getApi().deleteModel({ + url: options.url || this.url, + requestOptions: omit(options, ['immediate', 'url']) + }).then(action(function () { + if (!options.immediate) { + removeFromStore(); + } + }))); + } + }, { + key: 'buildFetchData', + value: function buildFetchData(options) { + return Object.assign(this.__getApi().buildFetchModelParams(this), this.__fetchParams, options.data); + } + }, { + key: 'fetch', + value: function fetch() { + var _this18 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + invariant(!this.isNew, 'Trying to fetch model without id!'); + + var data = this.buildFetchData(options); + var promise = this.wrapPendingRequestCount(this.__getApi().fetchModel({ + url: options.url || this.url, + data: data, + requestOptions: omit(options, ['data', 'url']) + }).then(action(function (res) { + _this18.fromBackend(res); + }))); + + return promise; + } + }, { + key: 'clear', + value: function clear() { + var _this19 = this; + + forIn(this.__originalAttributes, function (value, key) { + _this19[key] = value; + }); + + this.__activeCurrentRelations.forEach(function (currentRel) { + _this19[currentRel].clear(); + }); + } + }, { + key: 'hasUserChanges', + get: function get$$1() { + var _this20 = this; + + if (this.__changes.length > 0) { + return true; + } + return this.__activeCurrentRelations.some(function (rel) { + return _this20[rel].hasUserChanges; + }); + } + }, { + key: 'fieldFilter', + get: function get$$1() { + var pickFields = this.pickFields(); + var omitFields = this.omitFields(); + + return function (name) { + return (!pickFields || pickFields.includes(name)) && !omitFields.includes(name); + }; + } + }, { + key: 'backendValidationErrors', + get: function get$$1() { + return this.__backendValidationErrors; + } + }], [{ + key: 'toBackendAttrKey', + value: function toBackendAttrKey(attrKey) { + return camelToSnake(attrKey); + } + + // In the frontend we don't want to deal with those snake_case attr names. + + }, { + key: 'fromBackendAttrKey', + value: function fromBackendAttrKey(attrKey) { + return snakeToCamel(attrKey); + } + }]); + return Model; +}(), _class2$1.primaryKey = 'id', _class2$1.backendResourceName = '', _class2$1.fileFields = [], _class2$1.pickFields = undefined, _class2$1.omitFields = [], _temp$1), (_descriptor$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__backendValidationErrors', [observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor2$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__pendingRequestCount', [observable], { + enumerable: true, + initializer: function initializer() { + return 0; + } +}), _descriptor3$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fetchParams', [observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor4$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__changes', [observable], { + enumerable: true, + initializer: function initializer() { + return []; + } +}), _descriptor5$1 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileChanges', [observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor6 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileDeletions', [observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _descriptor7 = _applyDecoratedDescriptor$1(_class$1.prototype, '__fileExists', [observable], { + enumerable: true, + initializer: function initializer() { + return {}; + } +}), _applyDecoratedDescriptor$1(_class$1.prototype, 'url', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'url'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isNew', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isNew'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isLoading'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '__parseRelations', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '__parseRelations'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'hasUserChanges'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fieldFilter', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fieldFilter'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fromBackend'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parse'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'save', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'save'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'setInput', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'saveAll', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'saveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parseValidationErrors', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parseValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clearValidationErrors', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clearValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'backendValidationErrors', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'backendValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'delete', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'delete'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fetch'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clear'), _class$1.prototype)), _class$1); + +// Function ripped from Django docs. +// See: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax +function csrfSafeMethod(method) { + // These HTTP methods do not require CSRF protection. + return (/^(GET|HEAD|OPTIONS|TRACE)$/i.test(method) + ); +} + +var BinderApi = function () { + function BinderApi() { + classCallCheck(this, BinderApi); + this.baseUrl = null; + this.csrfToken = null; + this.defaultHeaders = {}; + this.axios = axios.create(); + + this.__initializeCsrfHandling(); + } + + createClass(BinderApi, [{ + key: '__initializeCsrfHandling', + value: function __initializeCsrfHandling() { + var _this = this; + + this.axios.interceptors.response.use(null, function (err) { + var status = get(err, 'response.status'); + var statusErrCode = get(err, 'response.data.code'); + var doNotRetry = get(err, 'response.config.doNotRetry'); + if (status === 403 && statusErrCode === 'CSRFFailure' && !doNotRetry) { + return _this.fetchCsrfToken().then(function () { + return _this.axios(_extends({}, err.response.config, { + doNotRetry: true + })); + }); + } + return Promise.reject(err); + }); + } + }, { + key: '__request', + value: function __request(method, url, data, options) { + options || (options = {}); + var useCsrfToken = csrfSafeMethod(method) ? undefined : this.csrfToken; + this.__testUrl(url); + + var axiosOptions = { + method: method, + baseURL: this.baseUrl, + url: url, + data: method !== 'get' && data ? data : undefined, + params: method === 'get' && data ? data : options.params + }; + + Object.assign(axiosOptions, options); + + // Don't clear existing headers when adding `options.headers` + var headers = Object.assign({ + 'Content-Type': 'application/json', + 'X-Csrftoken': useCsrfToken + }, this.defaultHeaders, options.headers); + axiosOptions.headers = headers; + + var xhr = this.axios(axiosOptions); + + // We fork the promise tree as we want to have the error traverse to the listeners + if (this.onRequestError && options.skipRequestError !== true) { + xhr.catch(this.onRequestError); + } + + var onSuccess = options.skipFormatter === true ? Promise.resolve() : this.__responseFormatter; + return xhr.then(onSuccess); + } + }, { + key: 'parseBackendValidationErrors', + value: function parseBackendValidationErrors(response) { + var valErrors = get(response, 'data.errors'); + if (response.status === 400 && valErrors) { + return valErrors; + } + return null; + } + }, { + key: 'fetchCsrfToken', + value: function fetchCsrfToken() { + var _this2 = this; + + return this.get('/api/bootstrap/').then(function (res) { + _this2.csrfToken = res.csrf_token; + }); + } + }, { + key: '__responseFormatter', + value: function __responseFormatter(res) { + return res.data; + } + }, { + key: '__testUrl', + value: function __testUrl(url) { + if (!url.endsWith('/')) { + throw new Error('Binder does not accept urls that do not have a trailing slash: ' + url); + } + } + }, { + key: 'get', + value: function get$$1(url, data, options) { + return this.__request('get', url, data, options); + } + }, { + key: 'post', + value: function post(url, data, options) { + return this.__request('post', url, data, options); + } + }, { + key: 'patch', + value: function patch(url, data, options) { + return this.__request('patch', url, data, options); + } + }, { + key: 'put', + value: function put(url, data, options) { + return this.__request('put', url, data, options); + } + }, { + key: 'delete', + value: function _delete(url, data, options) { + return this.__request('delete', url, data, options); + } + }, { + key: 'buildFetchModelParams', + value: function buildFetchModelParams(model) { + return { + // TODO: I really dislike that this is comma separated and not an array. + // We should fix this in the Binder API. + with: model.__activeRelations.map(model.constructor.toBackendAttrKey).join(',') || null + }; + } + }, { + key: 'fetchModel', + value: function fetchModel(_ref) { + var url = _ref.url, + data = _ref.data, + requestOptions = _ref.requestOptions; + + return this.get(url, data, requestOptions).then(function (res) { + return { + data: res.data, + repos: res.with, + relMapping: res.with_mapping, + reverseRelMapping: res.with_related_name_mapping + }; + }); + } + }, { + key: 'saveModel', + value: function saveModel(_ref2) { + var _this3 = this; + + var url = _ref2.url, + data = _ref2.data, + isNew = _ref2.isNew, + requestOptions = _ref2.requestOptions; + + var method = isNew ? 'post' : 'patch'; + return this[method](url, data, requestOptions).then(function (newData) { + return { data: newData }; + }).catch(function (err) { + if (err.response) { + err.valErrors = _this3.parseBackendValidationErrors(err.response); + } + throw err; + }); + } + }, { + key: 'saveAllModels', + value: function saveAllModels(_ref3) { + var _this4 = this; + + var url = _ref3.url, + data = _ref3.data, + model = _ref3.model, + requestOptions = _ref3.requestOptions; + + return this.put(url, { + data: data.data, + with: data.relations + }, requestOptions).then(function (res) { + if (res.idmap) { + model.__parseNewIds(res.idmap); + } + return res; + }).catch(function (err) { + if (err.response) { + err.valErrors = _this4.parseBackendValidationErrors(err.response); + } + throw err; + }); + } + }, { + key: 'deleteModel', + value: function deleteModel(_ref4) { + var url = _ref4.url, + requestOptions = _ref4.requestOptions; + + // TODO: kind of silly now, but we'll probably want better error handling soon. + return this.delete(url, null, requestOptions); + } + }, { + key: 'buildFetchStoreParams', + value: function buildFetchStoreParams(store) { + var offset = store.getPageOffset(); + var limit = store.__state.limit; + return { + with: store.__activeRelations.map(store.Model.toBackendAttrKey).join(',') || null, + limit: limit === null ? 'none' : limit, + // Hide offset if zero so the request looks cleaner in DevTools. + offset: offset || null + }; + } + }, { + key: 'fetchStore', + value: function fetchStore(_ref5) { + var url = _ref5.url, + data = _ref5.data, + requestOptions = _ref5.requestOptions; + + return this.get(url, data, requestOptions).then(function (res) { + return { + response: res, + data: res.data, + repos: res.with, + relMapping: res.with_mapping, + reverseRelMapping: res.with_related_name_mapping, + totalRecords: res.meta.total_records + }; + }); + } + }]); + return BinderApi; +}(); + +var DATE_LIB = 'moment'; +var SUPPORTED_DATE_LIBS = ['moment', 'luxon']; + +function configureDateLib(dateLib) { + invariant(SUPPORTED_DATE_LIBS.includes(dateLib), 'Unsupported date lib `' + dateLib + '`. ' + ('(Supported: ' + SUPPORTED_DATE_LIBS.map(function (dateLib) { + return '`' + dateLib + '`'; + }).join(', ') + ')')); + DATE_LIB = dateLib; +} + +function checkMomentInstance(attr, value) { + invariant(moment.isMoment(value), 'Attribute `' + attr + '` is not a moment instance.'); +} + +function checkLuxonDateTime(attr, value) { + invariant(moment.isMoment(value), 'Attribute `' + attr + '` is not a luxon DateTime.'); +} + +var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; +var LUXON_DATETIME_FORMAT = 'yyy-LL-ddTHH:mm:ssZZZ'; + +var CASTS = { + momentDate: { + parse: function parse(attr, value) { + if (value === null || value === undefined) { + return null; + } + return moment(value, 'YYYY-MM-DD'); + }, + toJS: function toJS$$1(attr, value) { + if (value === null || value === undefined) { + return null; + } + checkMomentInstance(attr, value); + return value.format('YYYY-MM-DD'); + }, + + dateLib: 'moment' + }, + momentDatetime: { + parse: function parse(attr, value) { + if (value === null) { + return null; + } + return moment(value); + }, + toJS: function toJS$$1(attr, value) { + if (value === null) { + return null; + } + checkMomentInstance(attr, value); + return value.toJSON(); // Use ISO8601 notation, adjusted to UTC + }, + + dateLib: 'moment' + }, + luxonDate: { + parse: function parse(attr, value) { + if (value === null || value === undefined) { + return null; + } + return DateTime.fromFormat(value, LUXON_DATE_FORMAT); + }, + toJS: function toJS$$1(attr, value) { + if (value === null || value === undefined) { + return null; + } + checkLuxonDateTime(attr, value); + return value.toFormat(LUXON_DATE_FORMAT); + }, + + dateLib: 'luxon' + }, + luxonDatetime: { + parse: function parse(attr, value) { + if (value === null) { + return null; + } + return DateTime.fromFormat(value, LUXON_DATETIME_FORMAT); + }, + toJS: function toJS$$1(attr, value) { + if (value === null) { + return null; + } + checkLuxonDateTime(attr, value); + return value.toFormat(LUXON_DATETIME_FORMAT); + }, + + dateLib: 'luxon' + }, + date: { + parse: function parse() { + var _CASTS$; + + return (_CASTS$ = CASTS[DATE_LIB + 'Date']).parse.apply(_CASTS$, arguments); + }, + toJS: function toJS$$1() { + var _CASTS$2; + + return (_CASTS$2 = CASTS[DATE_LIB + 'Date']).toJS.apply(_CASTS$2, arguments); + }, + + get dateLib() { + return DATE_LIB; + } + }, + datetime: { + parse: function parse() { + var _CASTS$3; + + return (_CASTS$3 = CASTS[DATE_LIB + 'Datetime']).parse.apply(_CASTS$3, arguments); + }, + toJS: function toJS$$1() { + var _CASTS$4; + + return (_CASTS$4 = CASTS[DATE_LIB + 'Datetime']).toJS.apply(_CASTS$4, arguments); + }, + + get dateLib() { + return DATE_LIB; + } + }, + enum: function _enum(expectedValues) { + invariant(isArray(expectedValues), 'Invalid argument suplied to `Casts.enum`, expected an instance of array.'); + function checkExpectedValues(attr, value) { + if (value === null) { + return null; + } + if (expectedValues.includes(value)) { + return value; + } + invariant(false, 'Value set to attribute `' + attr + '`, ' + JSON.stringify(value) + ', is not one of the allowed enum: ' + JSON.stringify(expectedValues)); + } + return { + parse: checkExpectedValues, + toJS: checkExpectedValues + }; + } +}; + +export { Model, Store, BinderApi, CASTS as Casts, configureDateLib }; From 505b6c2f70d94fcd81cc6d717c66273f23af49f3 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 14:00:31 +0200 Subject: [PATCH 20/45] Fix copy changes double '__' instead of '_' --- dist/mobx-spine.cjs.js | 8 ++++---- dist/mobx-spine.es.js | 8 ++++---- src/Model.js | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 678a10d..1c717e0 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1208,7 +1208,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set only the changed attributes if (copyChanges) { - copiedModel._copyChanges(source); + copiedModel.__copyChanges(source); } return copiedModel; @@ -1223,8 +1223,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { */ }, { - key: '_copyChanges', - value: function _copyChanges(source, store) { + key: '__copyChanges', + value: function __copyChanges(source, store) { var _this6 = this; // Maintain the relations after copy @@ -1253,7 +1253,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // If related item is a store // Set the changes for all related models with changes source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); }); } else { // Set the changes for the related model diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index a037d8b..2a2ae4f 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1202,7 +1202,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set only the changed attributes if (copyChanges) { - copiedModel._copyChanges(source); + copiedModel.__copyChanges(source); } return copiedModel; @@ -1217,8 +1217,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { */ }, { - key: '_copyChanges', - value: function _copyChanges(source, store) { + key: '__copyChanges', + value: function __copyChanges(source, store) { var _this6 = this; // Maintain the relations after copy @@ -1247,7 +1247,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // If related item is a store // Set the changes for all related models with changes source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index]._copyChanges(relatedModel, _this6[relation]); + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); }); } else { // Set the changes for the related model diff --git a/src/Model.js b/src/Model.js index 87ce470..4a19dc9 100644 --- a/src/Model.js +++ b/src/Model.js @@ -454,7 +454,7 @@ export default class Model { // Set only the changed attributes if (copyChanges) { - copiedModel._copyChanges(source) + copiedModel.__copyChanges(source) } return copiedModel; @@ -467,7 +467,7 @@ export default class Model { * @param store - the store of the current model, to setChanged if there are changes * @private */ - _copyChanges(source, store) { + __copyChanges(source, store) { // Maintain the relations after copy this.__activeRelations = source.__activeRelations; this.__activeCurrentRelations = source.__activeCurrentRelations; @@ -493,7 +493,7 @@ export default class Model { if (source[relation].models) { // If related item is a store // Set the changes for all related models with changes source[relation].models.forEach((relatedModel, index) => { - this[relation].models[index]._copyChanges(relatedModel, this[relation]); + this[relation].models[index].__copyChanges(relatedModel, this[relation]); }); } else { // Set the changes for the related model From 96f203ef899b8f48863f9f4d7c70b5744f349a05 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 17:43:52 +0200 Subject: [PATCH 21/45] Also copy related objects of related objects that were not properly defined --- dist/mobx-spine.cjs.js | 6 ++++++ dist/mobx-spine.es.js | 6 ++++++ src/Model.js | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 1c717e0..b53a94e 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1248,6 +1248,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach(function (relation) { if (relation && source[relation]) { + if (!_this6[relation]) { + // Sometimes a nested model has relations that were not defined in the starting object, + // these need to be copied as well + _this6[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); + _this6[relation].parse(source[relation].toJS()); + } if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 2a2ae4f..47d4d8b 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1242,6 +1242,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach(function (relation) { if (relation && source[relation]) { + if (!_this6[relation]) { + // Sometimes a nested model has relations that were not defined in the starting object, + // these need to be copied as well + _this6[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); + _this6[relation].parse(source[relation].toJS()); + } if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store diff --git a/src/Model.js b/src/Model.js index 4a19dc9..1a43c21 100644 --- a/src/Model.js +++ b/src/Model.js @@ -489,6 +489,12 @@ export default class Model { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach((relation) => { if (relation && source[relation]) { + if (!this[relation]){ + // Sometimes a nested model has relations that were not defined in the starting object, + // these need to be copied as well + this[relation] = source[relation].constructor({relations: source[relation].__activeRelations}); + this[relation].parse(source[relation].toJS()); + } if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store // Set the changes for all related models with changes From c9636d24d378c0a8d7cec3d089e2a3af8725b3d3 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 19:51:07 +0200 Subject: [PATCH 22/45] Added warning for relations on related models that were not defined in source model --- dist/mobx-spine.cjs.js | 31 +++++++++++++++---------------- dist/mobx-spine.es.js | 31 +++++++++++++++---------------- src/Model.js | 30 +++++++++++++++--------------- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index b53a94e..b26266b 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1248,23 +1248,22 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach(function (relation) { if (relation && source[relation]) { - if (!_this6[relation]) { - // Sometimes a nested model has relations that were not defined in the starting object, - // these need to be copied as well - _this6[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); - _this6[relation].parse(source[relation].toJS()); - } - if (source[relation].hasUserChanges) { - if (source[relation].models) { - // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); - }); - } else { - // Set the changes for the related model - _this6[relation].__copyChanges(source[relation], undefined); + if (_this6[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); + } } + } else { + // Related object not in relations of the model we are copying + console.warn('Found related object ' + source.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); } } }); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 47d4d8b..d71a523 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1242,23 +1242,22 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach(function (relation) { if (relation && source[relation]) { - if (!_this6[relation]) { - // Sometimes a nested model has relations that were not defined in the starting object, - // these need to be copied as well - _this6[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); - _this6[relation].parse(source[relation].toJS()); - } - if (source[relation].hasUserChanges) { - if (source[relation].models) { - // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); - }); - } else { - // Set the changes for the related model - _this6[relation].__copyChanges(source[relation], undefined); + if (_this6[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); + } } + } else { + // Related object not in relations of the model we are copying + console.warn('Found related object ' + source.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); } } }); diff --git a/src/Model.js b/src/Model.js index 1a43c21..bafeadc 100644 --- a/src/Model.js +++ b/src/Model.js @@ -489,22 +489,22 @@ export default class Model { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach((relation) => { if (relation && source[relation]) { - if (!this[relation]){ - // Sometimes a nested model has relations that were not defined in the starting object, - // these need to be copied as well - this[relation] = source[relation].constructor({relations: source[relation].__activeRelations}); - this[relation].parse(source[relation].toJS()); - } - if (source[relation].hasUserChanges) { - if (source[relation].models) { // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach((relatedModel, index) => { - this[relation].models[index].__copyChanges(relatedModel, this[relation]); - }); - } else { - // Set the changes for the related model - this[relation].__copyChanges(source[relation], undefined) + if (this[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach((relatedModel, index) => { + this[relation].models[index].__copyChanges(relatedModel, this[relation]); + }); + } else { + // Set the changes for the related model + this[relation].__copyChanges(source[relation], undefined) + } } + } else { + // Related object not in relations of the model we are copying + console.warn(`Found related object ${source.backendResourceName} with relation ${relation}, + which is not defined in the relations of the model you are copying. Skipping ${relation}.`) } } }); From 242351263de20f0deb04a01d1dc6896a7b12087e Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 22:14:58 +0200 Subject: [PATCH 23/45] Test using parseRelations function --- dist/mobx-spine.cjs.js | 6 ++---- dist/mobx-spine.es.js | 6 ++---- src/Model.js | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index b26266b..19d5f6b 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1200,7 +1200,6 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model @@ -1228,8 +1227,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var _this6 = this; // Maintain the relations after copy - this.__activeRelations = source.__activeRelations; - this.__activeCurrentRelations = source.__activeCurrentRelations; + this.__parseRelations(source.__activeRelations); // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { @@ -1263,7 +1261,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } } else { // Related object not in relations of the model we are copying - console.warn('Found related object ' + source.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); + console.warn('Found related object ' + source.constructor.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); } } }); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index d71a523..7937b25 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1194,7 +1194,6 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model @@ -1222,8 +1221,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var _this6 = this; // Maintain the relations after copy - this.__activeRelations = source.__activeRelations; - this.__activeCurrentRelations = source.__activeCurrentRelations; + this.__parseRelations(source.__activeRelations); // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { @@ -1257,7 +1255,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } } else { // Related object not in relations of the model we are copying - console.warn('Found related object ' + source.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); + console.warn('Found related object ' + source.constructor.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); } } }); diff --git a/src/Model.js b/src/Model.js index bafeadc..8ff53ea 100644 --- a/src/Model.js +++ b/src/Model.js @@ -445,7 +445,6 @@ export default class Model { // Maintain the relations after copy // this.__activeRelations = source.__activeRelations; - copiedModel.__activeCurrentRelations = source.__activeCurrentRelations; copiedModel.__parseRelations(source.__activeRelations); // Copy all fields and values from the specified model @@ -469,8 +468,7 @@ export default class Model { */ __copyChanges(source, store) { // Maintain the relations after copy - this.__activeRelations = source.__activeRelations; - this.__activeCurrentRelations = source.__activeCurrentRelations; + this.__parseRelations(source.__activeRelations); // Copy all changed fields and notify the store that there are changes if (source.__changes.length > 0) { @@ -503,7 +501,7 @@ export default class Model { } } else { // Related object not in relations of the model we are copying - console.warn(`Found related object ${source.backendResourceName} with relation ${relation}, + console.warn(`Found related object ${source.constructor.backendResourceName} with relation ${relation}, which is not defined in the relations of the model you are copying. Skipping ${relation}.`) } } From eb5f0968687c6e2bb37b353b860c46eaf9ec1c30 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 22:28:53 +0200 Subject: [PATCH 24/45] Try with deeper copy --- dist/mobx-spine.cjs.js | 31 ++++++++++++++++--------------- dist/mobx-spine.es.js | 31 ++++++++++++++++--------------- src/Model.js | 30 +++++++++++++++--------------- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 19d5f6b..5185365 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1246,22 +1246,23 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach(function (relation) { if (relation && source[relation]) { - if (_this6[relation]) { - if (source[relation].hasUserChanges) { - if (source[relation].models) { - // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); - }); - } else { - // Set the changes for the related model - _this6[relation].__copyChanges(source[relation], undefined); - } + if (!_this6[relation]) { + // Sometimes a nested model has relations that were not defined in the starting object, + // these need to be copied as well + _this6[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); + _this6[relation].parse(source[relation].toJS()); + } + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); } - } else { - // Related object not in relations of the model we are copying - console.warn('Found related object ' + source.constructor.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); } } }); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 7937b25..14bec41 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1240,22 +1240,23 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach(function (relation) { if (relation && source[relation]) { - if (_this6[relation]) { - if (source[relation].hasUserChanges) { - if (source[relation].models) { - // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); - }); - } else { - // Set the changes for the related model - _this6[relation].__copyChanges(source[relation], undefined); - } + if (!_this6[relation]) { + // Sometimes a nested model has relations that were not defined in the starting object, + // these need to be copied as well + _this6[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); + _this6[relation].parse(source[relation].toJS()); + } + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); } - } else { - // Related object not in relations of the model we are copying - console.warn('Found related object ' + source.constructor.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); } } }); diff --git a/src/Model.js b/src/Model.js index 8ff53ea..5880da2 100644 --- a/src/Model.js +++ b/src/Model.js @@ -487,22 +487,22 @@ export default class Model { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach((relation) => { if (relation && source[relation]) { - if (this[relation]) { - if (source[relation].hasUserChanges) { - if (source[relation].models) { // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach((relatedModel, index) => { - this[relation].models[index].__copyChanges(relatedModel, this[relation]); - }); - } else { - // Set the changes for the related model - this[relation].__copyChanges(source[relation], undefined) - } + if (!this[relation]) { + // Sometimes a nested model has relations that were not defined in the starting object, + // these need to be copied as well + this[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); + this[relation].parse(source[relation].toJS()); + } + if (source[relation].hasUserChanges) { + if (source[relation].models) { // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach((relatedModel, index) => { + this[relation].models[index].__copyChanges(relatedModel, this[relation]); + }); + } else { + // Set the changes for the related model + this[relation].__copyChanges(source[relation], undefined) } - } else { - // Related object not in relations of the model we are copying - console.warn(`Found related object ${source.constructor.backendResourceName} with relation ${relation}, - which is not defined in the relations of the model you are copying. Skipping ${relation}.`) } } }); From 21670095c01c2efe95c0f0b04113182dfa157088 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 22:38:03 +0200 Subject: [PATCH 25/45] new --- dist/mobx-spine.cjs.js | 2 +- dist/mobx-spine.es.js | 2 +- src/Model.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 5185365..39337a2 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1249,7 +1249,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (!_this6[relation]) { // Sometimes a nested model has relations that were not defined in the starting object, // these need to be copied as well - _this6[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); + _this6[relation] = new source[relation].constructor({ relations: source[relation].__activeRelations }); _this6[relation].parse(source[relation].toJS()); } if (source[relation].hasUserChanges) { diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 14bec41..a6544c0 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1243,7 +1243,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (!_this6[relation]) { // Sometimes a nested model has relations that were not defined in the starting object, // these need to be copied as well - _this6[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); + _this6[relation] = new source[relation].constructor({ relations: source[relation].__activeRelations }); _this6[relation].parse(source[relation].toJS()); } if (source[relation].hasUserChanges) { diff --git a/src/Model.js b/src/Model.js index 5880da2..495e3e8 100644 --- a/src/Model.js +++ b/src/Model.js @@ -490,7 +490,7 @@ export default class Model { if (!this[relation]) { // Sometimes a nested model has relations that were not defined in the starting object, // these need to be copied as well - this[relation] = source[relation].constructor({ relations: source[relation].__activeRelations }); + this[relation] = new source[relation].constructor({ relations: source[relation].__activeRelations }); this[relation].parse(source[relation].toJS()); } if (source[relation].hasUserChanges) { From e354afa8fba7da547822b3897a3a0c5f10f3fb52 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 22:49:22 +0200 Subject: [PATCH 26/45] Revert to previous implementation --- dist/mobx-spine.cjs.js | 31 +++++++++++++++---------------- dist/mobx-spine.es.js | 31 +++++++++++++++---------------- src/Model.js | 30 +++++++++++++++--------------- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 39337a2..19d5f6b 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1246,23 +1246,22 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach(function (relation) { if (relation && source[relation]) { - if (!_this6[relation]) { - // Sometimes a nested model has relations that were not defined in the starting object, - // these need to be copied as well - _this6[relation] = new source[relation].constructor({ relations: source[relation].__activeRelations }); - _this6[relation].parse(source[relation].toJS()); - } - if (source[relation].hasUserChanges) { - if (source[relation].models) { - // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); - }); - } else { - // Set the changes for the related model - _this6[relation].__copyChanges(source[relation], undefined); + if (_this6[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); + } } + } else { + // Related object not in relations of the model we are copying + console.warn('Found related object ' + source.constructor.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); } } }); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index a6544c0..7937b25 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1240,23 +1240,22 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach(function (relation) { if (relation && source[relation]) { - if (!_this6[relation]) { - // Sometimes a nested model has relations that were not defined in the starting object, - // these need to be copied as well - _this6[relation] = new source[relation].constructor({ relations: source[relation].__activeRelations }); - _this6[relation].parse(source[relation].toJS()); - } - if (source[relation].hasUserChanges) { - if (source[relation].models) { - // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); - }); - } else { - // Set the changes for the related model - _this6[relation].__copyChanges(source[relation], undefined); + if (_this6[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); + } } + } else { + // Related object not in relations of the model we are copying + console.warn('Found related object ' + source.constructor.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); } } }); diff --git a/src/Model.js b/src/Model.js index 495e3e8..8ff53ea 100644 --- a/src/Model.js +++ b/src/Model.js @@ -487,22 +487,22 @@ export default class Model { // Set the changes for all related models with changes source.__activeCurrentRelations.forEach((relation) => { if (relation && source[relation]) { - if (!this[relation]) { - // Sometimes a nested model has relations that were not defined in the starting object, - // these need to be copied as well - this[relation] = new source[relation].constructor({ relations: source[relation].__activeRelations }); - this[relation].parse(source[relation].toJS()); - } - if (source[relation].hasUserChanges) { - if (source[relation].models) { // If related item is a store - // Set the changes for all related models with changes - source[relation].models.forEach((relatedModel, index) => { - this[relation].models[index].__copyChanges(relatedModel, this[relation]); - }); - } else { - // Set the changes for the related model - this[relation].__copyChanges(source[relation], undefined) + if (this[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { // If related item is a store + // Set the changes for all related models with changes + source[relation].models.forEach((relatedModel, index) => { + this[relation].models[index].__copyChanges(relatedModel, this[relation]); + }); + } else { + // Set the changes for the related model + this[relation].__copyChanges(source[relation], undefined) + } } + } else { + // Related object not in relations of the model we are copying + console.warn(`Found related object ${source.constructor.backendResourceName} with relation ${relation}, + which is not defined in the relations of the model you are copying. Skipping ${relation}.`) } } }); From e3a6872bf8b796db035c36b9fbe875df3095866a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 31 Mar 2021 23:23:51 +0200 Subject: [PATCH 27/45] Also copy store changes --- dist/mobx-spine.cjs.js | 2 ++ dist/mobx-spine.es.js | 2 ++ src/Model.js | 2 ++ 3 files changed, 6 insertions(+) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 19d5f6b..0efb14a 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1250,6 +1250,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store + // Check if the store has some changes + _this6[relation].__setChanged = source[relation].__setChanged; // Set the changes for all related models with changes source[relation].models.forEach(function (relatedModel, index) { _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 7937b25..e50f93b 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1244,6 +1244,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store + // Check if the store has some changes + _this6[relation].__setChanged = source[relation].__setChanged; // Set the changes for all related models with changes source[relation].models.forEach(function (relatedModel, index) { _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); diff --git a/src/Model.js b/src/Model.js index 8ff53ea..468a932 100644 --- a/src/Model.js +++ b/src/Model.js @@ -490,6 +490,8 @@ export default class Model { if (this[relation]) { if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store + // Check if the store has some changes + this[relation].__setChanged = source[relation].__setChanged; // Set the changes for all related models with changes source[relation].models.forEach((relatedModel, index) => { this[relation].models[index].__copyChanges(relatedModel, this[relation]); From 5c934ce9df11f2e8815597423b0037d782ae46c8 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 8 Apr 2021 16:38:38 +0200 Subject: [PATCH 28/45] More tests for model copy and validation --- src/__tests__/Model.js | 635 +++++++++++++++++++++++++++++++++++++-- src/__tests__/helpers.js | 51 ++++ 2 files changed, 656 insertions(+), 30 deletions(-) create mode 100644 src/__tests__/helpers.js diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index fee1439..e9bfe06 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -3,6 +3,7 @@ import { toJS, observable } from 'mobx'; import MockAdapter from 'axios-mock-adapter'; import _ from 'lodash'; import { Model, BinderApi } from '../'; +import { compareObjectsIgnoringNegativeIds } from "./helpers"; import { Animal, AnimalStore, @@ -105,6 +106,7 @@ test('property defined as both attribute and relation should throw error', () => test('initialize() method should be called', () => { const initMock = jest.fn(); + class Zebra extends Model { initialize() { initMock(); @@ -616,7 +618,7 @@ test('toBackendAll with model relation', () => { animal.kind.parse({ id: 5 }); const serialized = animal.toBackendAll({ - nestedRelations: {kind: { breed: {}}, owner: {}}, + nestedRelations: { kind: { breed: {} }, owner: {} }, }); expect(serialized).toMatchSnapshot(); }); @@ -644,7 +646,7 @@ test('toBackendAll with partial relations', () => { }, { relations: ['kind', 'owner.town'] } ); - const serialized = animal.toBackendAll({ nestedRelations: {owner: {}} }); + const serialized = animal.toBackendAll({ nestedRelations: { owner: {} } }); expect(serialized).toMatchSnapshot(); }); @@ -665,7 +667,7 @@ test('toBackendAll with store relation', () => { { id: 10, name: 'R' }, ]); - const serialized = animal.toBackendAll({ nestedRelations: {pastOwners: {}} }); + const serialized = animal.toBackendAll({ nestedRelations: { pastOwners: {} } }); expect(serialized).toMatchSnapshot(); }); @@ -682,7 +684,7 @@ test('toBackendAll should de-duplicate relations', () => { expect(animalBar.cid).toBe(animal.pastOwners.at(1).cid); const serialized = animal.toBackendAll({ - nestedRelations: {pastOwners: {town: {}}}, + nestedRelations: { pastOwners: { town: {} } }, }); expect(serialized).toMatchSnapshot(); }); @@ -701,7 +703,7 @@ test('toBackendAll with deep nested relation', () => { }); const serialized = animal.toBackendAll({ - nestedRelations: {kind: { location: {}, breed: { location: {} }}}, + nestedRelations: { kind: { location: {}, breed: { location: {} } } }, }); expect(serialized).toMatchSnapshot(); }); @@ -726,7 +728,7 @@ test('toBackendAll with nested store relation', () => { ]); const serialized = animal.toBackendAll({ - nestedRelations: {pastOwners: { town: {} }}, + nestedRelations: { pastOwners: { town: {} } }, }); expect(serialized).toMatchSnapshot(); }); @@ -755,7 +757,7 @@ test('toBackendAll with `backendResourceName` property model', () => { }); const serialized = animal.toBackendAll({ - nestedRelations: {blaat: {}, owners: {}, pastOwners: {}}, + nestedRelations: { blaat: {}, owners: {}, pastOwners: {} }, }); expect(serialized).toMatchSnapshot(); }); @@ -939,6 +941,7 @@ test('setInput to clear backend validation errors', () => { test('allow custom validationErrorFormatter', () => { const location = new class extends Location { static backendResourceName = 'location'; + validationErrorFormatter(obj) { return obj.msg; } @@ -1098,6 +1101,7 @@ describe('requests', () => { const myApi = new BinderApi(); mock.onAny().replyOnce(200, {}); const spy = jest.spyOn(myApi, 'get'); + class Zebra extends Model { static backendResourceName = 'zebra'; api = myApi; @@ -1143,6 +1147,26 @@ describe('requests', () => { }); }); + test('validate new with basic properties, should not save', () => { + const animal = new Animal({ name: 'Doggo' }); + const spy = jest.spyOn(animal, 'saveFromBackend'); + mock.onAny().replyOnce(config => { + expect(config.params).toEqual({ validate: true }); + expect(config.url).toBe('/api/animal/'); + expect(config.method).toBe('post'); + expect(config.data).toBe('{"id":null,"name":"Doggo"}'); + return [201, { id: 10, name: 'Doggo' }]; + }); + + return animal.validate().then(() => { + expect(animal.id).toBe(null); + expect(spy).not.toHaveBeenCalled(); + + spy.mockReset(); + spy.mockRestore(); + }); + }); + test('save existing with basic properties', () => { const animal = new Animal({ id: 12, name: 'Burhan' }); mock.onAny().replyOnce(config => { @@ -1168,6 +1192,22 @@ describe('requests', () => { }); }); + test('validation error with basic properties', () => { + const animal = new Animal({ name: 'Nope' }); + mock.onAny().replyOnce(config => { + expect(config.params).toEqual({ validate: true }); + return [400, saveFailData] + }); + + return animal.validate().catch(() => { + const valErrors = toJS(animal.backendValidationErrors); + expect(valErrors).toEqual({ + name: ['required'], + kind: ['blank'], + }); + }); + }); + test('save new model fail with basic properties', () => { const animal = new Animal({ name: 'Nope' }); mock.onAny().replyOnce(400, saveNewFailData); @@ -1180,6 +1220,21 @@ describe('requests', () => { }); }); + test('save new model validation error with basic properties', () => { + const animal = new Animal({ name: 'Nope' }); + mock.onAny().replyOnce(config => { + expect(config.params).toEqual({ validate: true }); + return [400, saveNewFailData] + }); + + return animal.validate().catch(() => { + const valErrors = toJS(animal.backendValidationErrors); + expect(valErrors).toEqual({ + name: ['invalid'], + }); + }); + }); + test('save fail with 500', () => { const animal = new Animal({ name: 'Nope' }); mock.onAny().replyOnce(500, {}); @@ -1190,6 +1245,19 @@ describe('requests', () => { }); }); + test('validation fail with 500', () => { + const animal = new Animal({ name: 'Nope' }); + mock.onAny().replyOnce(config => { + expect(config.params).toEqual({ validate: true }); + return [500, {}] + }); + + return animal.validate().catch(() => { + const valErrors = toJS(animal.backendValidationErrors); + expect(valErrors).toEqual({}); + }); + }); + test('save with params', () => { const animal = new Animal(); mock.onAny().replyOnce(config => { @@ -1258,6 +1326,35 @@ describe('requests', () => { }); }); + test('validate all with relations', () => { + const animal = new Animal( + { + name: 'Doggo', + kind: { name: 'Dog' }, + pastOwners: [{ name: 'Henk' }], + }, + { relations: ['kind', 'pastOwners'] } + ); + const spy = jest.spyOn(animal, 'saveFromBackend'); + mock.onAny().replyOnce(config => { + expect(config.params).toEqual({ validate: true }); + expect(config.url).toBe('/api/animal/'); + expect(config.method).toBe('put'); + return [201, animalMultiPutResponse]; + }); + + return animal.validateAll({ relations: ['kind'] }).then(response => { + expect(spy).not.toHaveBeenCalled(); + expect(animal.id).toBe(10); + expect(animal.kind.id).toBe(4); + expect(animal.pastOwners.at(0).id).toBe(100); + // expect(response).toEqual(animalMultiPutResponse); + + spy.mockReset(); + spy.mockRestore(); + }); + }); + test('save all with relations - verify ids are mapped correctly', () => { const animal = new Animal( { @@ -1294,7 +1391,46 @@ describe('requests', () => { }); return animal.saveAll({ relations: ['kind'] }).then( - () => {}, + () => { + }, + err => { + if (!err.response) { + throw err; + } + expect(toJS(animal.backendValidationErrors).name).toEqual([ + 'blank', + ]); + expect(toJS(animal.kind.backendValidationErrors).name).toEqual([ + 'required', + ]); + expect( + toJS(animal.pastOwners.at(0).backendValidationErrors).name + ).toEqual(['required']); + expect( + toJS(animal.pastOwners.at(0).town.backendValidationErrors) + .name + ).toEqual(['maxlength']); + } + ); + }); + + test('validate all with errors', () => { + const animal = new Animal( + { + name: 'Doggo', + kind: { name: 'Dog' }, + pastOwners: [{ name: 'Jo', town: { id: 5, name: '' } }], + }, + { relations: ['kind', 'pastOwners.town'] } + ); + mock.onAny().replyOnce(config => { + expect(config.params).toEqual({ validate: true }); + return [400, animalMultiPutError]; + }); + + return animal.validateAll({ relations: ['kind'] }).then( + () => { + }, err => { if (!err.response) { throw err; @@ -1333,7 +1469,47 @@ describe('requests', () => { const options = { relations: ['pastOwners.town'] }; return animal.saveAll(options).then( - () => {}, + () => { + }, + err => { + if (!err.response) { + throw err; + } + mock.onAny().replyOnce(200, { idmap: [] }); + return animal.saveAll(options).then(() => { + const valErrors1 = toJS( + animal.pastOwners.at(0).backendValidationErrors + ); + expect(valErrors1).toEqual({}); + const valErrors2 = toJS( + animal.pastOwners.at(0).town.backendValidationErrors + ); + expect(valErrors2).toEqual({}); + }); + } + ); + }); + + test('validate all with validation errors and check if it clears them', () => { + const animal = new Animal( + { + name: 'Doggo', + pastOwners: [{ name: 'Jo', town: { id: 5, name: '' } }], + }, + { relations: ['pastOwners.town'] } + ); + + // We first trigger a save with validation errors from the backend, then we trigger a second save which fixes those validation errors, + // then we check if the errors get cleared. + mock.onAny().replyOnce(config => { + expect(config.params).toEqual({ validate: true }); + return [400, animalMultiPutError]; + }); + + const options = { relations: ['pastOwners.town'] }; + return animal.validateAll(options).then( + () => { + }, err => { if (!err.response) { throw err; @@ -1542,10 +1718,12 @@ describe('requests', () => { test('hasUserChanges should not clear changes in non-saved models relations', () => { const animal = new Animal( - { id: 1, pastOwners: [ - { id: 2 }, - { id: 3 }, - ] }, + { + id: 1, pastOwners: [ + { id: 2 }, + { id: 3 }, + ] + }, { relations: ['pastOwners', 'kind.breed'] } ); @@ -1566,10 +1744,12 @@ describe('requests', () => { test('hasUserChanges should clear set changes in saved relations', () => { const animal = new Animal( - { id: 1, pastOwners: [ - { id: 2 }, - { id: 3 }, - ] }, + { + id: 1, pastOwners: [ + { id: 2 }, + { id: 3 }, + ] + }, { relations: ['pastOwners', 'kind.breed'] } ); @@ -1589,10 +1769,12 @@ describe('requests', () => { test('hasUserChanges should not clear set changes in non-saved relations', () => { const animal = new Animal( - { id: 1, pastOwners: [ - { id: 2 }, - { id: 3 }, - ] }, + { + id: 1, pastOwners: [ + { id: 2 }, + { id: 3 }, + ] + }, { relations: ['pastOwners', 'kind.breed'] } ); @@ -1663,7 +1845,7 @@ describe('changes', () => { const output = animal.toBackendAll({ // The `owner` relation is just here to verify that it is not included - nestedRelations: {kind: {breed: {}}, pastOwners: {}}, + nestedRelations: { kind: { breed: {} }, pastOwners: {} }, onlyChanges: true, }); expect(output).toEqual({ @@ -1709,7 +1891,7 @@ describe('changes', () => { const output = animal.toBackendAll({ // The `kind` and `breed` relations are just here to verify that they are not included - nestedRelations: {kind: {breed: {}}, pastOwners: {}}, + nestedRelations: { kind: { breed: {} }, pastOwners: {} }, onlyChanges: true, }); expect(output).toEqual({ @@ -1719,7 +1901,6 @@ describe('changes', () => { }); - test('toBackendAll should detect removed models', () => { const animal = new Animal( { @@ -1738,7 +1919,7 @@ describe('changes', () => { const output = animal.toBackendAll({ // The `kind` and `breed` relations are just here to verify that they are not included - nestedRelations: {kind: {breed: {}}, pastOwners: {}}, + nestedRelations: { kind: { breed: {} }, pastOwners: {} }, onlyChanges: true, }); expect(output).toEqual({ @@ -1763,7 +1944,7 @@ describe('changes', () => { { relations: ['kind.breed', 'owner', 'pastOwners'] } ); const output = animal.toBackendAll({ - nestedRelations: {kind: {breed: {}}, pastOwners: {}}, + nestedRelations: { kind: { breed: {} }, pastOwners: {} }, onlyChanges: false, }); expect(output).toEqual({ @@ -1841,7 +2022,7 @@ test('copy (with changes)', () => { customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); - const customerCopyWithChanges = new Customer(null, {relations: ['oldTowns.bestCook']}); + const customerCopyWithChanges = new Customer(); customerCopyWithChanges.copy(customer) // Clone with changes should give the same toBackend result as the cloned object @@ -1862,7 +2043,7 @@ test('copy (with changes without instantiating model)', () => { customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); - const customerCopyWithChanges = customer.copy({copyChanges: true}) + const customerCopyWithChanges = customer.copy({ copyChanges: true }) // Clone with changes should give the same toBackend result as the cloned object expect(customerCopyWithChanges.toBackendAll({ onlyChanges: true })).toEqual(customer.toBackendAll({ onlyChanges: true })) @@ -1902,11 +2083,405 @@ test('copy (without changes)', () => { customer.oldTowns.models[0].bestCook.workPlaces.models[0].setInput('name', "Italian"); const customerCopyNoChanges = new Customer(); - customerCopyNoChanges.copy(customer, {copyChanges: true}) + customerCopyNoChanges.copy(customer, { copyChanges: true }) // Clone without changes should give the same toBackend result as the cloned object when only changes is false - expect(customerCopyNoChanges.toBackendAll({onlyChanges: false})).toEqual(customer.toBackendAll({onlyChanges: false})) + expect(customerCopyNoChanges.toBackendAll({ onlyChanges: false })).toEqual(customer.toBackendAll({ onlyChanges: false })) +}); + +test('copy with store relation', () => { + const animal = new Animal({}, { relations: ['pastOwners'] }); + + animal.pastOwners.parse([ + { name: 'Bar' }, + { name: 'Foo' }, + { id: 10, name: 'R' }, + ]); + + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + + let serialized = copiedAnimal.toBackendAll({ nestedRelations: { pastOwners: {} } }); + let expected = animal.toBackendAll({ nestedRelations: { pastOwners: {} } }); + compareObjectsIgnoringNegativeIds(serialized, expected, expect) + + const animalAlternativeCopy = new Animal(); + animalAlternativeCopy.copy(animal); + + serialized = copiedAnimal.toBackendAll({ nestedRelations: { pastOwners: {} } }); + expected = animal.toBackendAll({ nestedRelations: { pastOwners: {} } }); + compareObjectsIgnoringNegativeIds(serialized, expected, expect) + }); +}); + +test('de-duplicate relations should not work after copy', () => { + const animal = new Animal({}, { relations: ['pastOwners.town'] }); + + animal.pastOwners.parse([{ name: 'Bar' }, { name: 'Foo' }]); + + // This is something you should never do, so maybe this is a bad test? + const animalBar = animal.pastOwners.at(0); + animal.pastOwners.models[1] = animalBar; + + // This isn't the real test, just a check. + expect(animalBar.cid).toBe(animal.pastOwners.at(1).cid); + + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + + let serialized = copiedAnimal.toBackendAll({ + nestedRelations: { pastOwners: { town: {} } }, + }); + let expected = animal.toBackendAll({ + nestedRelations: { pastOwners: { town: {} } }, + }); + // We should not copy cid's therefore it should not equal expected + compareObjectsIgnoringNegativeIds(serialized, expected, expect, false) + }); +}); + +test('copy with deep nested relation', () => { + // It's very important to test what happens when the same relation ('location') is used twice + is nested. + const animal = new Animal( + {}, + { relations: ['kind.location', 'kind.breed.location'] } + ); + + animal.kind.parse({ + name: 'Aap', + location: { name: 'Apenheul' }, + breed: { name: 'MyBreed', location: { name: 'Amerika' } }, + }); + + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + + const expected = animal.toBackendAll({ + nestedRelations: { kind: { location: {}, breed: { location: {} } } }, + }); + const serialized = copiedAnimal.toBackendAll({ + nestedRelations: { kind: { location: {}, breed: { location: {} } } }, + }); + compareObjectsIgnoringNegativeIds(serialized, expected, expect) + }); +}); + +test('copy with nested store relation', () => { + // It's very important to test what happens when the same relation ('location') is used twice + is nested. + const animal = new Animal({}, { relations: ['pastOwners.town'] }); + + animal.pastOwners.parse([ + { + name: 'Henk', + town: { + name: 'Eindhoven', + }, + }, + { + name: 'Krol', + town: { + name: 'Breda', + }, + }, + ]); + + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + + + const expected = animal.toBackendAll({ + nestedRelations: { pastOwners: { town: {} } }, + }); + const serialized = copiedAnimal.toBackendAll({ + nestedRelations: { pastOwners: { town: {} } }, + }); + compareObjectsIgnoringNegativeIds(serialized, expected, expect) + }); +}); + +test('toBackendAll with `backendResourceName` property model', () => { + const animal = new AnimalResourceName( + {}, + { relations: ['blaat', 'owners', 'pastOwners'] } + ); + + animal.parse({ + id: 1, + blaat: { + id: 2, + }, + owners: [ + { + id: 3, + }, + ], + pastOwners: [ + { + id: 4, + }, + ], + }); + + const copiedAnimal = animal.copy(); + const expected = animal.toBackendAll({ + nestedRelations: { blaat: {}, owners: {}, pastOwners: {} }, + }); + const serialized = copiedAnimal.toBackendAll({ + nestedRelations: { blaat: {}, owners: {}, pastOwners: {} }, + }); + compareObjectsIgnoringNegativeIds(serialized, expected, expect) + +}); + +describe('copy with changes', () => { + test('toBackend of copy should detect changes', () => { + const animal = new Animal( + { id: 1, name: 'Lino', kind: { id: 2 } }, + { relations: ['kind'] } + ); + + const output = animal.toBackend({ onlyChanges: true }); + expect(output).toEqual({ id: 1 }); + + animal.setInput('name', 'Lion'); + + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + + expect(toJS(copiedAnimal.__changes)).toEqual(['name']); + const output2 = copiedAnimal.toBackend({ onlyChanges: true }); + // `kind: 2` should not appear in here. + expect(output2).toEqual({ + id: 1, + name: 'Lion', + }); + }); + }); + + test('toBackend should detect changes - but not twice', () => { + const animal = new Animal({ id: 1 }); + + animal.setInput('name', 'Lino'); + animal.setInput('name', 'Lion'); + + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + expect(toJS(copiedAnimal.__changes)).toEqual(['name']); + const output = copiedAnimal.toBackend({ onlyChanges: true }); + expect(output).toEqual({ + id: 1, + name: 'Lion', + }); + }) + }); + + test('toBackendAll should detect changes', () => { + const animal = new Animal( + { + id: 1, + name: 'Lino', + kind: { + id: 2, + owner: { id: 4 }, + }, + pastOwners: [{ id: 5, name: 'Henk' }, { id: 6, name: 'Piet' }], + }, + { relations: ['kind.breed', 'owner', 'pastOwners'] } + ); + + animal.pastOwners.at(1).setInput('name', 'Jan'); + animal.kind.breed.setInput('name', 'Cat'); + + const output = animal.toBackendAll({ + // The `owner` relation is just here to verify that it is not included + nestedRelations: { kind: { breed: {} }, pastOwners: {} }, + onlyChanges: true, + }); + expect(output).toEqual({ + data: [{ id: 1, }], + relations: { + kind: [ + { + id: 2, + breed: -3, + }, + ], + breed: [ + { + id: -3, + name: 'Cat', + }, + ], + past_owners: [ + { + id: 6, + name: 'Jan', + } + ], + }, + }); + }); + + test('toBackendAll should detect added models', () => { + const animal = new Animal( + { + id: 1, + name: 'Lino', + kind: { + id: 2, + owner: { id: 4 }, + }, + pastOwners: [{ id: 5, name: 'Henk' }], + }, + { relations: ['kind.breed', 'owner', 'pastOwners'] } + ); + + animal.pastOwners.add({ id: 6 }); + + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + + const output = copiedAnimal.toBackendAll({ + // The `kind` and `breed` relations are just here to verify that they are not included + nestedRelations: { kind: { breed: {} }, pastOwners: {} }, + onlyChanges: true, + }); + expect(output).toEqual({ + data: [{ id: 1, past_owners: [5, 6] }], + relations: {}, + }); + }); + }); + + + test('toBackendAll should detect removed models', () => { + const animal = new Animal( + { + id: 1, + name: 'Lino', + kind: { + id: 2, + owner: { id: 4 }, + }, + pastOwners: [{ id: 5, name: 'Henk' }, { id: 6, name: 'Piet' }], + }, + { relations: ['kind.breed', 'owner', 'pastOwners'] } + ); + + animal.pastOwners.removeById(6); + + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + + const output = copiedAnimal.toBackendAll({ + // The `kind` and `breed` relations are just here to verify that they are not included + nestedRelations: { kind: { breed: {} }, pastOwners: {} }, + onlyChanges: true, + }); + expect(output).toEqual({ + data: [{ id: 1, past_owners: [5] }], + relations: {}, + }); + }); + }); + + + test('toBackendAll without onlyChanges should serialize all relations', () => { + const animal = new Animal( + { + id: 1, + name: 'Lino', + kind: { + id: 2, + breed: { name: 'Cat' }, + owner: { id: 4 }, + }, + pastOwners: [{ id: 5, name: 'Henk' }], + }, + { relations: ['kind.breed', 'owner', 'pastOwners'] } + ); + + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal, index) => { + const output = copiedAnimal.toBackendAll({ + nestedRelations: { kind: { breed: {} }, pastOwners: {} }, + onlyChanges: false, + }); + expect(output).toEqual({ + data: [{ + id: 1, + name: 'Lino', + kind: 2, + owner: null, + past_owners: [5] + }], + relations: { + kind: [ + { + id: 2, + // We don't care that our other copy gets a different id, as long as they are not the same + breed: index === 0 ? -8 : -13, + name: '', + }, + ], + breed: [ + { + id: index === 0 ? -8 : -13, + name: 'Cat', + }, + ], + past_owners: [{ + id: 5, + name: 'Henk' + }], + }, + }); + }); + }); + + test('hasUserChanges should detect changes in current fields', () => { + const animal = new Animal({ id: 1 }); + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + expect(copiedAnimal.hasUserChanges).toBe(false); + }); + + animal.setInput('name', 'Lino'); + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + expect(copiedAnimal.hasUserChanges).toBe(true); + }); + }); + + test('hasUserChanges should detect changes in model relations', () => { + const animal = new Animal({ id: 1 }, { relations: ['kind.breed'] }); + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + expect(copiedAnimal.hasUserChanges).toBe(false); + }); + + animal.kind.breed.setInput('name', 'Katachtige'); + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + expect(copiedAnimal.hasUserChanges).toBe(true); + }); + }); + + test('hasUserChanges should detect changes in store relations', () => { + const animal = new Animal( + { id: 1, pastOwners: [{ id: 1 }] }, + { relations: ['pastOwners'] } + ); + + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + expect(copiedAnimal.hasUserChanges).toBe(false); + }); + + animal.pastOwners.at(0).setInput('name', 'Henk'); + + // Should work for both copy methods + [animal.copy(), new Animal().copy(animal)].forEach((copiedAnimal) => { + expect(copiedAnimal.hasUserChanges).toBe(true); + }); + }); }); // test('validate', () => { diff --git a/src/__tests__/helpers.js b/src/__tests__/helpers.js new file mode 100644 index 0000000..151f06e --- /dev/null +++ b/src/__tests__/helpers.js @@ -0,0 +1,51 @@ +/** + * Takes an object and changes all negative numbers to be a check for a negative number so that you can check if two + * objects are the same except for the generated negative ids which can be different. + * + * @param expected + */ +export function modifyObjectNegativeIdCheck(object){ + Object.keys(object).forEach((key) => { + if (object[key] < 0){ + // If value + object[key] = expect.any(Number); + } else if (Array.isArray(object[key])) { + // If list + modifyListNegativeIdCheck(object[key]); + } else if (typeof object[key] === 'object' && object[key] !== null){ + // If object + modifyObjectNegativeIdCheck(object[key]); + } + }) +} + +/** + * Takes a list and changes all negative numbers to be a check for a negative number so that you can check if two + * lists or the lists inside of an object are the same except for the generated negative ids which can be different. + * @param expected + */ +function modifyListNegativeIdCheck(expected){ + debugger + Array.prototype.forEach.call(expected,(item) => { + if (item < 0){ + // If value + expected[expected.indexOf(item)] = expect.any(Number); + } else if (Array.isArray(item)) { + // If list + modifyListNegativeIdCheck(item); + } else if (typeof item === 'object' && item !== null){ + // If object + modifyObjectNegativeIdCheck(item); + } + }) +} + +export function compareObjectsIgnoringNegativeIds(object, toEqual, expect, bool = true){ + const expected = toEqual + modifyObjectNegativeIdCheck(expected); + if (bool === true) { + expect(object).toMatchObject(expected); + } else { + expect(object).not.toMatchObject(expected); + } +} From 661a8c499bf8b2edf5b824aa28286404cd3a56be Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 8 Apr 2021 16:45:50 +0200 Subject: [PATCH 29/45] Extra comments --- src/__tests__/helpers.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/__tests__/helpers.js b/src/__tests__/helpers.js index 151f06e..f580ee6 100644 --- a/src/__tests__/helpers.js +++ b/src/__tests__/helpers.js @@ -40,6 +40,13 @@ function modifyListNegativeIdCheck(expected){ }) } +/** + * Checks if 2 objects are the same ignoring negative ids + * @param object - The first object you want to compare + * @param toEqual - The second object you want to compare the first object to + * @param expect - The expect of the test to do the actual comparison + * @param bool - True if the objects should be the same, false otherwise (default: true) + */ export function compareObjectsIgnoringNegativeIds(object, toEqual, expect, bool = true){ const expected = toEqual modifyObjectNegativeIdCheck(expected); From 22cb52e56aad1d3dcbfe793202e2e99557c6ff2c Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Jun 2021 10:27:23 +0200 Subject: [PATCH 30/45] Make isNew work for negative ids as well --- src/Model.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Model.js b/src/Model.js index 468a932..239321f 100644 --- a/src/Model.js +++ b/src/Model.js @@ -109,22 +109,36 @@ export default class Model { return -parseInt(this.cid.replace('m', '')); } + /** + * Get InternalId returns the id of a model or a negative id if the id is not set + * @returns {*} - the id of a model or a negative id if the id is not set + */ getInternalId() { - if (this.isNew) { + if (!this[this.constructor.primaryKey]) { return this.getNegativeId(); } return this[this.constructor.primaryKey]; } + /** + * The get url returns the url for a model., it appends the id if there is one. If the model is new it should not + * append an id. + * + * @returns {string} - the url for a model + */ @computed get url() { const id = this[this.constructor.primaryKey]; - return `${result(this, 'urlRoot')}${id ? `${id}/` : ''}`; + return `${result(this, 'urlRoot')}${!this.isNew ? `${id}/` : ''}`; } + /** + * A model is considered new if it does not have an id, or if the id is a negative integer. + * @returns {boolean} True if the model id is not set or a negative integer + */ @computed get isNew() { - return !this[this.constructor.primaryKey]; + return !this[this.constructor.primaryKey] || this[this.constructor.primaryKey] < 0; } @computed From 53179ecbb40002f03b0baccdf0816eab539f0318 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Jun 2021 10:31:24 +0200 Subject: [PATCH 31/45] Add build --- dist/mobx-spine.cjs.js | 26 +++++++++++++++++++++++--- dist/mobx-spine.es.js | 26 +++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 9787def..bf26283 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -857,14 +857,28 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function getNegativeId() { return -parseInt(this.cid.replace('m', '')); } + + /** + * Get InternalId returns the id of a model or a negative id if the id is not set + * @returns {*} - the id of a model or a negative id if the id is not set + */ + }, { key: 'getInternalId', value: function getInternalId() { - if (this.isNew) { + if (!this[this.constructor.primaryKey]) { return this.getNegativeId(); } return this[this.constructor.primaryKey]; } + + /** + * The get url returns the url for a model., it appends the id if there is one. If the model is new it should not + * append an id. + * + * @returns {string} - the url for a model + */ + }, { key: 'casts', value: function casts() { @@ -895,12 +909,18 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { key: 'url', get: function get$$1() { var id = this[this.constructor.primaryKey]; - return '' + lodash.result(this, 'urlRoot') + (id ? id + '/' : ''); + return '' + lodash.result(this, 'urlRoot') + (!this.isNew ? id + '/' : ''); } + + /** + * A model is considered new if it does not have an id, or if the id is a negative integer. + * @returns {boolean} True if the model id is not set or a negative integer + */ + }, { key: 'isNew', get: function get$$1() { - return !this[this.constructor.primaryKey]; + return !this[this.constructor.primaryKey] || this[this.constructor.primaryKey] < 0; } }, { key: 'isLoading', diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index d0c7427..0f8f680 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -851,14 +851,28 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function getNegativeId() { return -parseInt(this.cid.replace('m', '')); } + + /** + * Get InternalId returns the id of a model or a negative id if the id is not set + * @returns {*} - the id of a model or a negative id if the id is not set + */ + }, { key: 'getInternalId', value: function getInternalId() { - if (this.isNew) { + if (!this[this.constructor.primaryKey]) { return this.getNegativeId(); } return this[this.constructor.primaryKey]; } + + /** + * The get url returns the url for a model., it appends the id if there is one. If the model is new it should not + * append an id. + * + * @returns {string} - the url for a model + */ + }, { key: 'casts', value: function casts() { @@ -889,12 +903,18 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { key: 'url', get: function get$$1() { var id = this[this.constructor.primaryKey]; - return '' + result(this, 'urlRoot') + (id ? id + '/' : ''); + return '' + result(this, 'urlRoot') + (!this.isNew ? id + '/' : ''); } + + /** + * A model is considered new if it does not have an id, or if the id is a negative integer. + * @returns {boolean} True if the model id is not set or a negative integer + */ + }, { key: 'isNew', get: function get$$1() { - return !this[this.constructor.primaryKey]; + return !this[this.constructor.primaryKey] || this[this.constructor.primaryKey] < 0; } }, { key: 'isLoading', From 1e998e0668f21c230f076f1a2edb10e62c027d7a Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Jun 2021 16:42:40 +0200 Subject: [PATCH 32/45] Add assignInternalId function as well as some tests --- src/Model.js | 8 ++++++++ src/__tests__/Model.js | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/Model.js b/src/Model.js index 239321f..89c3683 100644 --- a/src/Model.js +++ b/src/Model.js @@ -120,6 +120,14 @@ export default class Model { return this[this.constructor.primaryKey]; } + /** + * Gives the model the internal id. This is useful if you have a new model that you want to give an id so + * that it can be referred to in a relation. + */ + assignInternalId() { + this[this.constructor.primaryKey] = this.getInternalId() + } + /** * The get url returns the url for a model., it appends the id if there is one. If the model is new it should not * append an id. diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index ded7132..ae201d7 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -149,6 +149,21 @@ test('isNew should be true for new model', () => { expect(animal.isNew).toBe(true); }); +test('isNew should be true for a model with negative id', () => { + const animal = new Animal(); + animal.id = animal.getInternalId(); + + expect(animal.isNew).toBe(true); +}); + +test('isNew should be true for a model that we assign an internal id', () => { + const animal = new Animal(); + animal.assignInternalId(); + + expect(animal.isNew).toBe(true); +}); + + test('isNew should be false for existing model', () => { const animal = new Animal({ id: 2 }); From 433d312eb9043d3092ba01e5700c4d31980a9c6b Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Jun 2021 16:44:53 +0200 Subject: [PATCH 33/45] With build --- dist/mobx-spine.cjs.js | 11 +++++++++++ dist/mobx-spine.es.js | 11 +++++++++++ src/__tests__/Model.js | 1 + 3 files changed, 23 insertions(+) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index bf26283..7974f87 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -872,6 +872,17 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return this[this.constructor.primaryKey]; } + /** + * Gives the model the internal id. This is useful if you have a new model that you want to give an id so + * that it can be referred to in a relation. + */ + + }, { + key: 'assignInternalId', + value: function assignInternalId() { + this[this.constructor.primaryKey] = this.getInternalId(); + } + /** * The get url returns the url for a model., it appends the id if there is one. If the model is new it should not * append an id. diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 0f8f680..cea489f 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -866,6 +866,17 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return this[this.constructor.primaryKey]; } + /** + * Gives the model the internal id. This is useful if you have a new model that you want to give an id so + * that it can be referred to in a relation. + */ + + }, { + key: 'assignInternalId', + value: function assignInternalId() { + this[this.constructor.primaryKey] = this.getInternalId(); + } + /** * The get url returns the url for a model., it appends the id if there is one. If the model is new it should not * append an id. diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index ae201d7..a766e46 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -161,6 +161,7 @@ test('isNew should be true for a model that we assign an internal id', () => { animal.assignInternalId(); expect(animal.isNew).toBe(true); + expect(animal.id).toBeLessThan(0); }); From 6b99f8db91b93590891a5d2a8648de4e8f5d1cb1 Mon Sep 17 00:00:00 2001 From: robinz Date: Fri, 11 Jun 2021 17:28:51 +0200 Subject: [PATCH 34/45] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48bda4c..e5c4b76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.1", + "version": "0.28.2", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From ba6c8fcaf793d0bfb571c05056f1356120bfa29c Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 11 Jun 2021 17:47:26 +0200 Subject: [PATCH 35/45] v0.28.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48bda4c..e5c4b76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.1", + "version": "0.28.2", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From 57fb1a077c1e1a226eff159d9f5a42ef193055fa Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 16 Jun 2021 16:29:18 +0200 Subject: [PATCH 36/45] Add extra safety check for copying stores --- dist/mobx-spine.cjs.js | 15 +++++++++------ dist/mobx-spine.es.js | 15 +++++++++------ src/Model.js | 14 ++++++++------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 7974f87..6597ae2 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1281,12 +1281,15 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store - // Check if the store has some changes - _this6[relation].__setChanged = source[relation].__setChanged; - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); - }); + if (source[relation].models.length === _this6[relation].models.length) { + // run only if the store shares the same amount of items + // Check if the store has some changes + _this6[relation].__setChanged = source[relation].__setChanged; + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } } else { // Set the changes for the related model _this6[relation].__copyChanges(source[relation], undefined); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index cea489f..c42e429 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1275,12 +1275,15 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store - // Check if the store has some changes - _this6[relation].__setChanged = source[relation].__setChanged; - // Set the changes for all related models with changes - source[relation].models.forEach(function (relatedModel, index) { - _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); - }); + if (source[relation].models.length === _this6[relation].models.length) { + // run only if the store shares the same amount of items + // Check if the store has some changes + _this6[relation].__setChanged = source[relation].__setChanged; + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } } else { // Set the changes for the related model _this6[relation].__copyChanges(source[relation], undefined); diff --git a/src/Model.js b/src/Model.js index 89c3683..4091e3e 100644 --- a/src/Model.js +++ b/src/Model.js @@ -512,12 +512,14 @@ export default class Model { if (this[relation]) { if (source[relation].hasUserChanges) { if (source[relation].models) { // If related item is a store - // Check if the store has some changes - this[relation].__setChanged = source[relation].__setChanged; - // Set the changes for all related models with changes - source[relation].models.forEach((relatedModel, index) => { - this[relation].models[index].__copyChanges(relatedModel, this[relation]); - }); + if (source[relation].models.length === this[relation].models.length) { // run only if the store shares the same amount of items + // Check if the store has some changes + this[relation].__setChanged = source[relation].__setChanged; + // Set the changes for all related models with changes + source[relation].models.forEach((relatedModel, index) => { + this[relation].models[index].__copyChanges(relatedModel, this[relation]); + }); + } } else { // Set the changes for the related model this[relation].__copyChanges(source[relation], undefined) From 10dfe761ccc75f08e3c32dd996e58e2cbb59cfba Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 16 Jun 2021 16:38:56 +0200 Subject: [PATCH 37/45] Move helpers file... oopsie my bad --- src/__tests__/Model.js | 2 +- src/__tests__/{ => helpers}/helpers.js | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/__tests__/{ => helpers}/helpers.js (100%) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index a766e46..a07b445 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -3,7 +3,7 @@ import { toJS, observable } from 'mobx'; import MockAdapter from 'axios-mock-adapter'; import _ from 'lodash'; import { Model, BinderApi, Casts } from '../'; -import { compareObjectsIgnoringNegativeIds } from "./helpers"; +import { compareObjectsIgnoringNegativeIds } from "./helpers/helpers"; import { Animal, AnimalStore, diff --git a/src/__tests__/helpers.js b/src/__tests__/helpers/helpers.js similarity index 100% rename from src/__tests__/helpers.js rename to src/__tests__/helpers/helpers.js From 2b14d9187e90ab0b654d80d8afd1a6d14dd0c9f0 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 16 Jun 2021 16:44:38 +0200 Subject: [PATCH 38/45] Skip testing the helpers.js --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e5c4b76..98d07cd 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "./src" ], "testPathIgnorePatterns": [ - "/fixtures/" + "/fixtures/", + "/helpers/" ] } } From 8f155f79aac387db3343f1cba85f088739b6d167 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 16 Jun 2021 16:45:00 +0200 Subject: [PATCH 39/45] v0.28.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98d07cd..1a89a68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.2", + "version": "0.28.3", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From d7d70580754b77a200d79906df104e626246778b Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 2 Jul 2021 17:43:40 +0200 Subject: [PATCH 40/45] Add catch to validate --- dist/mobx-spine.cjs.js | 4 ++-- dist/mobx-spine.es.js | 4 ++-- src/Model.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 6597ae2..5307b5d 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1586,7 +1586,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } else { options.params = { validate: true }; } - return this.save(options); + return this.save(options).catch(function () {}); } }, { key: 'save', @@ -1720,7 +1720,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } else { options.params = { validate: true }; } - return this.saveAll(options); + return this.saveAll(options).catch(function () {}); } }, { key: 'saveAll', diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index c42e429..de5409f 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1580,7 +1580,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } else { options.params = { validate: true }; } - return this.save(options); + return this.save(options).catch(function () {}); } }, { key: 'save', @@ -1714,7 +1714,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } else { options.params = { validate: true }; } - return this.saveAll(options); + return this.saveAll(options).catch(function () {}); } }, { key: 'saveAll', diff --git a/src/Model.js b/src/Model.js index 4091e3e..6d50377 100644 --- a/src/Model.js +++ b/src/Model.js @@ -798,7 +798,7 @@ export default class Model { } else { options.params = { validate: true }; } - return this.save(options); + return this.save(options).catch(()=>{}); } @action @@ -911,7 +911,7 @@ export default class Model { } else { options.params = { validate: true }; } - return this.saveAll(options); + return this.saveAll(options).catch(()=>{}); } @action From bbdc5b82ee18e795b11bae82d470a4ee98f54379 Mon Sep 17 00:00:00 2001 From: robinz Date: Sat, 3 Jul 2021 00:11:03 +0200 Subject: [PATCH 41/45] v0.28.4 --- dist/mobx-spine.cjs.js | 8 ++++++-- dist/mobx-spine.es.js | 8 ++++++-- package.json | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 5307b5d..8b69dc2 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1586,7 +1586,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } else { options.params = { validate: true }; } - return this.save(options).catch(function () {}); + return this.save(options).catch(function (err) { + throw err; + }); } }, { key: 'save', @@ -1720,7 +1722,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } else { options.params = { validate: true }; } - return this.saveAll(options).catch(function () {}); + return this.saveAll(options).catch(function (err) { + throw err; + }); } }, { key: 'saveAll', diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index de5409f..15a5c6e 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1580,7 +1580,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } else { options.params = { validate: true }; } - return this.save(options).catch(function () {}); + return this.save(options).catch(function (err) { + throw err; + }); } }, { key: 'save', @@ -1714,7 +1716,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } else { options.params = { validate: true }; } - return this.saveAll(options).catch(function () {}); + return this.saveAll(options).catch(function (err) { + throw err; + }); } }, { key: 'saveAll', diff --git a/package.json b/package.json index 1a89a68..847a76e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.3", + "version": "0.28.4", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From 28dffb9e2b243b83ea10c7d4541cc65fd4cc0429 Mon Sep 17 00:00:00 2001 From: robinz Date: Sat, 3 Jul 2021 00:11:15 +0200 Subject: [PATCH 42/45] Throw the caught error --- src/Model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model.js b/src/Model.js index 6d50377..172cefd 100644 --- a/src/Model.js +++ b/src/Model.js @@ -798,7 +798,7 @@ export default class Model { } else { options.params = { validate: true }; } - return this.save(options).catch(()=>{}); + return this.save(options).catch((err)=>{throw err}); } @action @@ -911,7 +911,7 @@ export default class Model { } else { options.params = { validate: true }; } - return this.saveAll(options).catch(()=>{}); + return this.saveAll(options).catch((err)=>{throw err}); } @action From 8a27546119262821ff106006d8f9053e3ad1908e Mon Sep 17 00:00:00 2001 From: Zaico Date: Wed, 28 Jul 2021 14:16:15 +0200 Subject: [PATCH 43/45] Remove debug artifact --- src/__tests__/helpers/helpers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/helpers/helpers.js b/src/__tests__/helpers/helpers.js index f580ee6..f6f72e0 100644 --- a/src/__tests__/helpers/helpers.js +++ b/src/__tests__/helpers/helpers.js @@ -25,7 +25,6 @@ export function modifyObjectNegativeIdCheck(object){ * @param expected */ function modifyListNegativeIdCheck(expected){ - debugger Array.prototype.forEach.call(expected,(item) => { if (item < 0){ // If value From 8d72cc2583b4300f4481c06de1e8213f3db7b2b5 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 28 Jul 2021 14:36:57 +0200 Subject: [PATCH 44/45] Merge with master fix --- src/__tests__/Model.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index b7e2fb9..fd44e67 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1369,7 +1369,7 @@ describe('requests', () => { return [201, animalMultiPutResponse]; }); - return animal.validateAll({ relations: ['kind'] }).then(response => { + return animal.validate({ relations: ['kind'] }).then(response => { expect(spy).not.toHaveBeenCalled(); expect(animal.id).toBe(10); expect(animal.kind.id).toBe(4); @@ -1502,7 +1502,7 @@ describe('requests', () => { throw err; } mock.onAny().replyOnce(200, { idmap: [] }); - return animal.saveAll(options).then(() => { + return animal.save(options).then(() => { const valErrors1 = toJS( animal.pastOwners.at(0).backendValidationErrors ); @@ -1533,7 +1533,7 @@ describe('requests', () => { }); const options = { relations: ['pastOwners.town'] }; - return animal.validateAll(options).then( + return animal.validate(options).then( () => { }, err => { From 0c57d210a3ec7f067620737a6d39d1f7866b7b87 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 26 Feb 2024 15:30:08 +0100 Subject: [PATCH 45/45] Update health_master with latest master --- dist/mobx-spine.cjs.js | 333 +++++++++++++++++++++++++++++++---------- dist/mobx-spine.es.js | 333 +++++++++++++++++++++++++++++++---------- 2 files changed, 508 insertions(+), 158 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 13ac9a7..1a4527e 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -924,14 +924,39 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function getNegativeId() { return -parseInt(this.cid.replace('m', '')); } + + /** + * Get InternalId returns the id of a model or a negative id if the id is not set + * @returns {*} - the id of a model or a negative id if the id is not set + */ + }, { key: 'getInternalId', value: function getInternalId() { - if (this.isNew) { + if (!this[this.constructor.primaryKey]) { return this.getNegativeId(); } return this[this.constructor.primaryKey]; } + + /** + * Gives the model the internal id. This is useful if you have a new model that you want to give an id so + * that it can be referred to in a relation. + */ + + }, { + key: 'assignInternalId', + value: function assignInternalId() { + this[this.constructor.primaryKey] = this.getInternalId(); + } + + /** + * The get url returns the url for a model., it appends the id if there is one. If the model is new it should not + * append an id. + * + * @returns {string} - the url for a model + */ + }, { key: 'casts', value: function casts() { @@ -962,12 +987,18 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { key: 'url', get: function get() { var id = this[this.constructor.primaryKey]; - return '' + lodash.result(this, 'urlRoot') + (id ? id + '/' : ''); + return '' + lodash.result(this, 'urlRoot') + (!this.isNew ? id + '/' : ''); } + + /** + * A model is considered new if it does not have an id, or if the id is a negative integer. + * @returns {boolean} True if the model id is not set or a negative integer + */ + }, { key: 'isNew', get: function get() { - return !this[this.constructor.primaryKey]; + return !this[this.constructor.primaryKey] || this[this.constructor.primaryKey] < 0; } }, { key: 'isLoading', @@ -1062,7 +1093,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { activeRelations.forEach(function (aRel) { // If aRel is null, this relation is already defined by another aRel // IE.: town.restaurants.chef && town - if (aRel === null) { + if (aRel === null || !!_this3[aRel]) { return; } var relNames = aRel.match(RE_SPLIT_FIRST_RELATION); @@ -1080,7 +1111,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { _this3.__activeCurrentRelations.push(currentRel); } }); - mobx.extendObservable(this, lodash.mapValues(relModels, function (otherRelNames, relName) { + // extendObservable where we omit the fields that are already created from other relations + mobx.extendObservable(this, lodash.mapValues(lodash.omit(relModels, Object.keys(relModels).filter(function (rel) { + return !!_this3[rel]; + })), function (otherRelNames, relName) { var RelModel = relations[relName]; invariant(RelModel, 'Specified relation "' + relName + '" does not exist on model.'); var options = { relations: otherRelNames }; @@ -1234,18 +1268,129 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return { data: [data], relations: relations }; } + + /** + * Makes this model a copy of the specified model + * or returns a copy of the current model when no model to copy is given + * It also clones the changes that were in the specified model. + * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. + * Cloning + * + * @param source {Model} - The model that should be copied + * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} + */ + + }, { + key: 'copy', + value: function copy() { + var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; + + var copiedModel = void 0; + // If our source is not a model it is 'probably' the options + if (source !== undefined && !(source instanceof Model)) { + options = source; + source = undefined; + } + + // Make sure that we have the correct model + if (source === undefined) { + source = this; + copiedModel = new source.constructor({ relations: source.__activeRelations }); + } else if (this.constructor !== source.constructor) { + copiedModel = new source.constructor({ relations: source.__activeRelations }); + } else { + copiedModel = this; + } + + var copyChanges = options.copyChanges; + + // Maintain the relations after copy + // this.__activeRelations = source.__activeRelations; + + copiedModel.__parseRelations(source.__activeRelations); + // Copy all fields and values from the specified model + copiedModel.parse(source.toJS()); + + // Set only the changed attributes + if (copyChanges) { + copiedModel.__copyChanges(source); + } + + return copiedModel; + } + + /** + * Goes over model and all related models to set the changed values and notify the store + * + * @param source - the model to copy + * @param store - the store of the current model, to setChanged if there are changes + * @private + */ + + }, { + key: '__copyChanges', + value: function __copyChanges(source, store) { + var _this6 = this; + + // Maintain the relations after copy + this.__parseRelations(source.__activeRelations); + + // Copy all changed fields and notify the store that there are changes + if (source.__changes.length > 0) { + if (store) { + store.__setChanged = true; + } else if (this.__store) { + this.__store.__setChanged = true; + } + + source.__changes.forEach(function (changedAttribute) { + _this6.setInput(changedAttribute, source[changedAttribute]); + }); + } + // Undefined safety + if (source.__activeCurrentRelations.length > 0) { + // Set the changes for all related models with changes + source.__activeCurrentRelations.forEach(function (relation) { + if (relation && source[relation]) { + if (_this6[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + if (source[relation].models.length === _this6[relation].models.length) { + // run only if the store shares the same amount of items + // Check if the store has some changes + _this6[relation].__setChanged = source[relation].__setChanged; + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); + } + } + } else { + // Related object not in relations of the model we are copying + console.warn('Found related object ' + source.constructor.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); + } + } + }); + } + } }, { key: 'toJS', value: function toJS() { - var _this6 = this; + var _this7 = this; var output = {}; this.__attributes.forEach(function (attr) { - output[attr] = _this6.__toJSAttr(attr, _this6[attr]); + output[attr] = _this7.__toJSAttr(attr, _this7[attr]); }); this.__activeCurrentRelations.forEach(function (currentRel) { - var model = _this6[currentRel]; + var model = _this7[currentRel]; if (model) { output[currentRel] = model.toJS(); } @@ -1308,7 +1453,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__scopeBackendResponse', value: function __scopeBackendResponse(_ref3) { - var _this7 = this; + var _this8 = this; var data = _ref3.data, targetRelName = _ref3.targetRelName, @@ -1330,18 +1475,18 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var repository = repos[repoName]; // For backwards compatibility, reverseMapping is optional (for now) var reverseRelName = reverseMapping ? reverseMapping[backendRelName] : null; - var relName = _this7.constructor.fromBackendAttrKey(backendRelName); + var relName = _this8.constructor.fromBackendAttrKey(backendRelName); if (targetRelName === relName) { - var relKey = data[_this7.constructor.toBackendAttrKey(relName)]; + var relKey = data[_this8.constructor.toBackendAttrKey(relName)]; if (relKey !== undefined) { relevant = true; - scopedData = _this7.__parseRepositoryToData(relKey, repository); + scopedData = _this8.__parseRepositoryToData(relKey, repository); } else if (repository && reverseRelName) { - var pk = data[_this7.constructor.primaryKey]; + var pk = data[_this8.constructor.primaryKey]; relevant = true; - scopedData = _this7.__parseReverseRepositoryToData(reverseRelName, pk, repository); - if (_this7.relations(relName).prototype instanceof Model) { + scopedData = _this8.__parseReverseRepositoryToData(reverseRelName, pk, repository); + if (_this8.relations(relName).prototype instanceof Model) { if (scopedData.length === 0) { scopedData = null; } else if (scopedData.length === 1) { @@ -1381,7 +1526,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fromBackend', value: function fromBackend(_ref4) { - var _this8 = this; + var _this9 = this; var data = _ref4.data, repos = _ref4.repos, @@ -1394,8 +1539,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // So when we have a model with a `town.restaurants.chef` relation, // we call fromBackend on the `town` relation. lodash.each(this.__activeCurrentRelations, function (relName) { - var rel = _this8[relName]; - var resScoped = _this8.__scopeBackendResponse({ + var rel = _this9[relName]; + var resScoped = _this9.__scopeBackendResponse({ data: data, targetRelName: relName, repos: repos, @@ -1437,22 +1582,22 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parse', value: function parse(data) { - var _this9 = this; + var _this10 = this; invariant(lodash.isPlainObject(data), 'Parameter supplied to `parse()` is not an object, got: ' + JSON.stringify(data)); lodash.forIn(data, function (value, key) { - var attr = _this9.constructor.fromBackendAttrKey(key); - if (_this9.__attributes.includes(attr)) { - _this9[attr] = _this9.__parseAttr(attr, value); - } else if (_this9.__activeCurrentRelations.includes(attr)) { + var attr = _this10.constructor.fromBackendAttrKey(key); + if (_this10.__attributes.includes(attr)) { + _this10[attr] = _this10.__parseAttr(attr, value); + } else if (_this10.__activeCurrentRelations.includes(attr)) { // In Binder, a relation property is an `int` or `[int]`, referring to its ID. // However, it can also be an object if there are nested relations (non flattened). if (lodash.isPlainObject(value) || Array.isArray(value) && value.every(lodash.isPlainObject)) { - _this9[attr].parse(value); + _this10[attr].parse(value); } else if (value === null) { // The relation is cleared. - _this9[attr].clear(); + _this10[attr].clear(); } } }); @@ -1472,7 +1617,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'saveFile', value: function saveFile(name) { - var _this10 = this; + var _this11 = this; var snakeName = camelToSnake(name); @@ -1483,16 +1628,16 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data.append(name, file, file.name); return this.api.post('' + this.url + snakeName + '/', data, { headers: { 'Content-Type': 'multipart/form-data' } }).then(mobx.action(function (res) { - _this10.__fileExists[name] = true; - delete _this10.__fileChanges[name]; - _this10.saveFromBackend(res); + _this11.__fileExists[name] = true; + delete _this11.__fileChanges[name]; + _this11.saveFromBackend(res); })); } else if (this.__fileDeletions[name]) { if (this.__fileExists[name]) { return this.api.delete('' + this.url + snakeName + '/').then(mobx.action(function () { - _this10.__fileExists[name] = false; - delete _this10.__fileDeletions[name]; - _this10.saveFromBackend({ data: defineProperty({}, snakeName, null) }); + _this11.__fileExists[name] = false; + delete _this11.__fileDeletions[name]; + _this11.saveFromBackend({ data: defineProperty({}, snakeName, null) }); })); } else { delete this.__fileDeletions[name]; @@ -1582,6 +1727,30 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return Promise.all(promises); } + + /** + * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal save request, example: {onlyChanges: true} + */ + + }, { + key: 'validate', + value: function validate() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate parameter + if (options.params) { + options.params.validate = true; + } else { + options.params = { validate: true }; + } + + return this.save(options).catch(function (err) { + throw err; + }); + } }, { key: 'save', value: function save() { @@ -1596,7 +1765,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '_save', value: function _save() { - var _this11 = this; + var _this12 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1612,17 +1781,20 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { isNew: this.isNew, requestOptions: lodash.omit(options, 'url', 'data', 'mapData') }).then(mobx.action(function (res) { - _this11.saveFromBackend(_extends({}, res, { - data: lodash.omit(res.data, _this11.fileFields().map(camelToSnake)) - })); - _this11.clearUserFieldChanges(); - return _this11.saveFiles().then(function () { - _this11.clearUserFileChanges(); - return Promise.resolve(res); - }); + // Only update the model when we are actually trying to save + if (!options.params || !options.params.validate) { + _this12.saveFromBackend(_extends({}, res, { + data: lodash.omit(res.data, _this12.fileFields().map(camelToSnake)) + })); + _this12.clearUserFieldChanges(); + return _this12.saveFiles().then(function () { + _this12.clearUserFileChanges(); + return Promise.resolve(res); + }); + } })).catch(mobx.action(function (err) { if (err.valErrors) { - _this11.parseValidationErrors(err.valErrors); + _this12.parseValidationErrors(err.valErrors); } throw err; }))); @@ -1630,7 +1802,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '_saveAll', value: function _saveAll() { - var _this12 = this; + var _this13 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1646,31 +1818,34 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }), requestOptions: lodash.omit(options, 'relations', 'data', 'mapData') }).then(mobx.action(function (res) { - _this12.saveFromBackend(res); - _this12.clearUserFieldChanges(); - - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); + // Only update the models if we are actually trying to save + if (!options.params || !options.params.validate) { + _this13.saveFromBackend(res); + _this13.clearUserFieldChanges(); - return _this12.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this12.clearUserFileChanges(); - - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { if (relation instanceof Model) { - relation.clearUserFileChanges(); + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); } }); - return res; - }); + return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + _this13.clearUserFileChanges(); + + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFileChanges(); + } + }); + + return res; + }); + } })).catch(mobx.action(function (err) { if (err.valErrors) { - _this12.parseValidationErrors(err.valErrors); + _this13.parseValidationErrors(err.valErrors); } throw err; }))); @@ -1682,19 +1857,19 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__parseNewIds', value: function __parseNewIds(idMaps) { - var _this13 = this; + var _this14 = this; var bName = this.constructor.backendResourceName; if (bName && idMaps[bName]) { var idMap = idMaps[bName].find(function (ids) { - return ids[0] === _this13.getInternalId(); + return ids[0] === _this14.getInternalId(); }); if (idMap) { this[this.constructor.primaryKey] = idMap[1]; } } lodash.each(this.__activeCurrentRelations, function (relName) { - var rel = _this13[relName]; + var rel = _this14[relName]; rel.__parseNewIds(idMaps); }); } @@ -1706,7 +1881,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parseValidationErrors', value: function parseValidationErrors(valErrors) { - var _this14 = this; + var _this15 = this; var bname = this.constructor.backendResourceName; @@ -1719,24 +1894,24 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return snakeToCamel(key); }); var formattedErrors = lodash.mapValues(camelCasedErrors, function (valError) { - return valError.map(_this14.validationErrorFormatter); + return valError.map(_this15.validationErrorFormatter); }); this.__backendValidationErrors = formattedErrors; } } this.__activeCurrentRelations.forEach(function (currentRel) { - _this14[currentRel].parseValidationErrors(valErrors); + _this15[currentRel].parseValidationErrors(valErrors); }); } }, { key: 'clearValidationErrors', value: function clearValidationErrors() { - var _this15 = this; + var _this16 = this; this.__backendValidationErrors = {}; this.__activeCurrentRelations.forEach(function (currentRel) { - _this15[currentRel].clearValidationErrors(); + _this16[currentRel].clearValidationErrors(); }); } @@ -1754,12 +1929,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'delete', value: function _delete() { - var _this16 = this; + var _this17 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var removeFromStore = function removeFromStore() { - return _this16.__store ? _this16.__store.remove(_this16) : null; + return _this17.__store ? _this17.__store.remove(_this17) : null; }; if (options.immediate || this.isNew) { removeFromStore(); @@ -1785,7 +1960,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fetch', value: function fetch() { - var _this17 = this; + var _this18 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1801,7 +1976,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data: data, requestOptions: lodash.omit(options, ['data', 'url']) }).then(mobx.action(function (res) { - _this17.fromBackend(res); + _this18.fromBackend(res); })).catch(function (e) { if (Axios.isCancel(e)) { return null; @@ -1815,14 +1990,14 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'clear', value: function clear() { - var _this18 = this; + var _this19 = this; lodash.forIn(this.__originalAttributes, function (value, key) { - _this18[key] = value; + _this19[key] = value; }); this.__activeCurrentRelations.forEach(function (currentRel) { - _this18[currentRel].clear(); + _this19[currentRel].clear(); }); } @@ -1843,13 +2018,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'hasUserChanges', get: function get() { - var _this19 = this; + var _this20 = this; if (this.__changes.length > 0) { return true; } return this.__activeCurrentRelations.some(function (rel) { - return _this19[rel].hasUserChanges; + return _this20[rel].hasUserChanges; }); } }, { @@ -2257,7 +2432,7 @@ function checkLuxonDateTime(attr, value) { } var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; -var LUXON_DATETIME_FORMAT = "yyyy'-'LL'-'dd'T'HH':'mm':'ssZZ"; +var LUXON_DATETIME_FORMAT = 'yyyy\'-\'LL\'-\'dd\'T\'HH\':\'mm\':\'ssZZ'; var CASTS = { momentDate: { diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 44436ae..3636d14 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -918,14 +918,39 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function getNegativeId() { return -parseInt(this.cid.replace('m', '')); } + + /** + * Get InternalId returns the id of a model or a negative id if the id is not set + * @returns {*} - the id of a model or a negative id if the id is not set + */ + }, { key: 'getInternalId', value: function getInternalId() { - if (this.isNew) { + if (!this[this.constructor.primaryKey]) { return this.getNegativeId(); } return this[this.constructor.primaryKey]; } + + /** + * Gives the model the internal id. This is useful if you have a new model that you want to give an id so + * that it can be referred to in a relation. + */ + + }, { + key: 'assignInternalId', + value: function assignInternalId() { + this[this.constructor.primaryKey] = this.getInternalId(); + } + + /** + * The get url returns the url for a model., it appends the id if there is one. If the model is new it should not + * append an id. + * + * @returns {string} - the url for a model + */ + }, { key: 'casts', value: function casts() { @@ -956,12 +981,18 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { key: 'url', get: function get() { var id = this[this.constructor.primaryKey]; - return '' + result(this, 'urlRoot') + (id ? id + '/' : ''); + return '' + result(this, 'urlRoot') + (!this.isNew ? id + '/' : ''); } + + /** + * A model is considered new if it does not have an id, or if the id is a negative integer. + * @returns {boolean} True if the model id is not set or a negative integer + */ + }, { key: 'isNew', get: function get() { - return !this[this.constructor.primaryKey]; + return !this[this.constructor.primaryKey] || this[this.constructor.primaryKey] < 0; } }, { key: 'isLoading', @@ -1056,7 +1087,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { activeRelations.forEach(function (aRel) { // If aRel is null, this relation is already defined by another aRel // IE.: town.restaurants.chef && town - if (aRel === null) { + if (aRel === null || !!_this3[aRel]) { return; } var relNames = aRel.match(RE_SPLIT_FIRST_RELATION); @@ -1074,7 +1105,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { _this3.__activeCurrentRelations.push(currentRel); } }); - extendObservable(this, mapValues(relModels, function (otherRelNames, relName) { + // extendObservable where we omit the fields that are already created from other relations + extendObservable(this, mapValues(omit(relModels, Object.keys(relModels).filter(function (rel) { + return !!_this3[rel]; + })), function (otherRelNames, relName) { var RelModel = relations[relName]; invariant(RelModel, 'Specified relation "' + relName + '" does not exist on model.'); var options = { relations: otherRelNames }; @@ -1228,18 +1262,129 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return { data: [data], relations: relations }; } + + /** + * Makes this model a copy of the specified model + * or returns a copy of the current model when no model to copy is given + * It also clones the changes that were in the specified model. + * Cloning the changes requires recursion over all related models that have changes or are related to a model with changes. + * Cloning + * + * @param source {Model} - The model that should be copied + * @param options {{}} - Options, {copyChanges - only copy the changed attributes, requires recursion over all related objects with changes} + */ + + }, { + key: 'copy', + value: function copy() { + var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { copyChanges: true }; + + var copiedModel = void 0; + // If our source is not a model it is 'probably' the options + if (source !== undefined && !(source instanceof Model)) { + options = source; + source = undefined; + } + + // Make sure that we have the correct model + if (source === undefined) { + source = this; + copiedModel = new source.constructor({ relations: source.__activeRelations }); + } else if (this.constructor !== source.constructor) { + copiedModel = new source.constructor({ relations: source.__activeRelations }); + } else { + copiedModel = this; + } + + var copyChanges = options.copyChanges; + + // Maintain the relations after copy + // this.__activeRelations = source.__activeRelations; + + copiedModel.__parseRelations(source.__activeRelations); + // Copy all fields and values from the specified model + copiedModel.parse(source.toJS()); + + // Set only the changed attributes + if (copyChanges) { + copiedModel.__copyChanges(source); + } + + return copiedModel; + } + + /** + * Goes over model and all related models to set the changed values and notify the store + * + * @param source - the model to copy + * @param store - the store of the current model, to setChanged if there are changes + * @private + */ + + }, { + key: '__copyChanges', + value: function __copyChanges(source, store) { + var _this6 = this; + + // Maintain the relations after copy + this.__parseRelations(source.__activeRelations); + + // Copy all changed fields and notify the store that there are changes + if (source.__changes.length > 0) { + if (store) { + store.__setChanged = true; + } else if (this.__store) { + this.__store.__setChanged = true; + } + + source.__changes.forEach(function (changedAttribute) { + _this6.setInput(changedAttribute, source[changedAttribute]); + }); + } + // Undefined safety + if (source.__activeCurrentRelations.length > 0) { + // Set the changes for all related models with changes + source.__activeCurrentRelations.forEach(function (relation) { + if (relation && source[relation]) { + if (_this6[relation]) { + if (source[relation].hasUserChanges) { + if (source[relation].models) { + // If related item is a store + if (source[relation].models.length === _this6[relation].models.length) { + // run only if the store shares the same amount of items + // Check if the store has some changes + _this6[relation].__setChanged = source[relation].__setChanged; + // Set the changes for all related models with changes + source[relation].models.forEach(function (relatedModel, index) { + _this6[relation].models[index].__copyChanges(relatedModel, _this6[relation]); + }); + } + } else { + // Set the changes for the related model + _this6[relation].__copyChanges(source[relation], undefined); + } + } + } else { + // Related object not in relations of the model we are copying + console.warn('Found related object ' + source.constructor.backendResourceName + ' with relation ' + relation + ',\n which is not defined in the relations of the model you are copying. Skipping ' + relation + '.'); + } + } + }); + } + } }, { key: 'toJS', value: function toJS() { - var _this6 = this; + var _this7 = this; var output = {}; this.__attributes.forEach(function (attr) { - output[attr] = _this6.__toJSAttr(attr, _this6[attr]); + output[attr] = _this7.__toJSAttr(attr, _this7[attr]); }); this.__activeCurrentRelations.forEach(function (currentRel) { - var model = _this6[currentRel]; + var model = _this7[currentRel]; if (model) { output[currentRel] = model.toJS(); } @@ -1302,7 +1447,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__scopeBackendResponse', value: function __scopeBackendResponse(_ref3) { - var _this7 = this; + var _this8 = this; var data = _ref3.data, targetRelName = _ref3.targetRelName, @@ -1324,18 +1469,18 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var repository = repos[repoName]; // For backwards compatibility, reverseMapping is optional (for now) var reverseRelName = reverseMapping ? reverseMapping[backendRelName] : null; - var relName = _this7.constructor.fromBackendAttrKey(backendRelName); + var relName = _this8.constructor.fromBackendAttrKey(backendRelName); if (targetRelName === relName) { - var relKey = data[_this7.constructor.toBackendAttrKey(relName)]; + var relKey = data[_this8.constructor.toBackendAttrKey(relName)]; if (relKey !== undefined) { relevant = true; - scopedData = _this7.__parseRepositoryToData(relKey, repository); + scopedData = _this8.__parseRepositoryToData(relKey, repository); } else if (repository && reverseRelName) { - var pk = data[_this7.constructor.primaryKey]; + var pk = data[_this8.constructor.primaryKey]; relevant = true; - scopedData = _this7.__parseReverseRepositoryToData(reverseRelName, pk, repository); - if (_this7.relations(relName).prototype instanceof Model) { + scopedData = _this8.__parseReverseRepositoryToData(reverseRelName, pk, repository); + if (_this8.relations(relName).prototype instanceof Model) { if (scopedData.length === 0) { scopedData = null; } else if (scopedData.length === 1) { @@ -1375,7 +1520,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fromBackend', value: function fromBackend(_ref4) { - var _this8 = this; + var _this9 = this; var data = _ref4.data, repos = _ref4.repos, @@ -1388,8 +1533,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // So when we have a model with a `town.restaurants.chef` relation, // we call fromBackend on the `town` relation. each(this.__activeCurrentRelations, function (relName) { - var rel = _this8[relName]; - var resScoped = _this8.__scopeBackendResponse({ + var rel = _this9[relName]; + var resScoped = _this9.__scopeBackendResponse({ data: data, targetRelName: relName, repos: repos, @@ -1431,22 +1576,22 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parse', value: function parse(data) { - var _this9 = this; + var _this10 = this; invariant(isPlainObject(data), 'Parameter supplied to `parse()` is not an object, got: ' + JSON.stringify(data)); forIn(data, function (value, key) { - var attr = _this9.constructor.fromBackendAttrKey(key); - if (_this9.__attributes.includes(attr)) { - _this9[attr] = _this9.__parseAttr(attr, value); - } else if (_this9.__activeCurrentRelations.includes(attr)) { + var attr = _this10.constructor.fromBackendAttrKey(key); + if (_this10.__attributes.includes(attr)) { + _this10[attr] = _this10.__parseAttr(attr, value); + } else if (_this10.__activeCurrentRelations.includes(attr)) { // In Binder, a relation property is an `int` or `[int]`, referring to its ID. // However, it can also be an object if there are nested relations (non flattened). if (isPlainObject(value) || Array.isArray(value) && value.every(isPlainObject)) { - _this9[attr].parse(value); + _this10[attr].parse(value); } else if (value === null) { // The relation is cleared. - _this9[attr].clear(); + _this10[attr].clear(); } } }); @@ -1466,7 +1611,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'saveFile', value: function saveFile(name) { - var _this10 = this; + var _this11 = this; var snakeName = camelToSnake(name); @@ -1477,16 +1622,16 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data.append(name, file, file.name); return this.api.post('' + this.url + snakeName + '/', data, { headers: { 'Content-Type': 'multipart/form-data' } }).then(action(function (res) { - _this10.__fileExists[name] = true; - delete _this10.__fileChanges[name]; - _this10.saveFromBackend(res); + _this11.__fileExists[name] = true; + delete _this11.__fileChanges[name]; + _this11.saveFromBackend(res); })); } else if (this.__fileDeletions[name]) { if (this.__fileExists[name]) { return this.api.delete('' + this.url + snakeName + '/').then(action(function () { - _this10.__fileExists[name] = false; - delete _this10.__fileDeletions[name]; - _this10.saveFromBackend({ data: defineProperty({}, snakeName, null) }); + _this11.__fileExists[name] = false; + delete _this11.__fileDeletions[name]; + _this11.saveFromBackend({ data: defineProperty({}, snakeName, null) }); })); } else { delete this.__fileDeletions[name]; @@ -1576,6 +1721,30 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return Promise.all(promises); } + + /** + * Validates a model by sending a save request to binder with the validate header set. Binder will return the validation + * errors without actually committing the save + * + * @param options - same as for a normal save request, example: {onlyChanges: true} + */ + + }, { + key: 'validate', + value: function validate() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + // Add the validate parameter + if (options.params) { + options.params.validate = true; + } else { + options.params = { validate: true }; + } + + return this.save(options).catch(function (err) { + throw err; + }); + } }, { key: 'save', value: function save() { @@ -1590,7 +1759,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '_save', value: function _save() { - var _this11 = this; + var _this12 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1606,17 +1775,20 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { isNew: this.isNew, requestOptions: omit(options, 'url', 'data', 'mapData') }).then(action(function (res) { - _this11.saveFromBackend(_extends({}, res, { - data: omit(res.data, _this11.fileFields().map(camelToSnake)) - })); - _this11.clearUserFieldChanges(); - return _this11.saveFiles().then(function () { - _this11.clearUserFileChanges(); - return Promise.resolve(res); - }); + // Only update the model when we are actually trying to save + if (!options.params || !options.params.validate) { + _this12.saveFromBackend(_extends({}, res, { + data: omit(res.data, _this12.fileFields().map(camelToSnake)) + })); + _this12.clearUserFieldChanges(); + return _this12.saveFiles().then(function () { + _this12.clearUserFileChanges(); + return Promise.resolve(res); + }); + } })).catch(action(function (err) { if (err.valErrors) { - _this11.parseValidationErrors(err.valErrors); + _this12.parseValidationErrors(err.valErrors); } throw err; }))); @@ -1624,7 +1796,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '_saveAll', value: function _saveAll() { - var _this12 = this; + var _this13 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1640,31 +1812,34 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }), requestOptions: omit(options, 'relations', 'data', 'mapData') }).then(action(function (res) { - _this12.saveFromBackend(res); - _this12.clearUserFieldChanges(); - - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); + // Only update the models if we are actually trying to save + if (!options.params || !options.params.validate) { + _this13.saveFromBackend(res); + _this13.clearUserFieldChanges(); - return _this12.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this12.clearUserFileChanges(); - - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { if (relation instanceof Model) { - relation.clearUserFileChanges(); + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); } }); - return res; - }); + return _this13.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + _this13.clearUserFileChanges(); + + forNestedRelations(_this13, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFileChanges(); + } + }); + + return res; + }); + } })).catch(action(function (err) { if (err.valErrors) { - _this12.parseValidationErrors(err.valErrors); + _this13.parseValidationErrors(err.valErrors); } throw err; }))); @@ -1676,19 +1851,19 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__parseNewIds', value: function __parseNewIds(idMaps) { - var _this13 = this; + var _this14 = this; var bName = this.constructor.backendResourceName; if (bName && idMaps[bName]) { var idMap = idMaps[bName].find(function (ids) { - return ids[0] === _this13.getInternalId(); + return ids[0] === _this14.getInternalId(); }); if (idMap) { this[this.constructor.primaryKey] = idMap[1]; } } each(this.__activeCurrentRelations, function (relName) { - var rel = _this13[relName]; + var rel = _this14[relName]; rel.__parseNewIds(idMaps); }); } @@ -1700,7 +1875,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parseValidationErrors', value: function parseValidationErrors(valErrors) { - var _this14 = this; + var _this15 = this; var bname = this.constructor.backendResourceName; @@ -1713,24 +1888,24 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return snakeToCamel(key); }); var formattedErrors = mapValues(camelCasedErrors, function (valError) { - return valError.map(_this14.validationErrorFormatter); + return valError.map(_this15.validationErrorFormatter); }); this.__backendValidationErrors = formattedErrors; } } this.__activeCurrentRelations.forEach(function (currentRel) { - _this14[currentRel].parseValidationErrors(valErrors); + _this15[currentRel].parseValidationErrors(valErrors); }); } }, { key: 'clearValidationErrors', value: function clearValidationErrors() { - var _this15 = this; + var _this16 = this; this.__backendValidationErrors = {}; this.__activeCurrentRelations.forEach(function (currentRel) { - _this15[currentRel].clearValidationErrors(); + _this16[currentRel].clearValidationErrors(); }); } @@ -1748,12 +1923,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'delete', value: function _delete() { - var _this16 = this; + var _this17 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var removeFromStore = function removeFromStore() { - return _this16.__store ? _this16.__store.remove(_this16) : null; + return _this17.__store ? _this17.__store.remove(_this17) : null; }; if (options.immediate || this.isNew) { removeFromStore(); @@ -1779,7 +1954,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fetch', value: function fetch() { - var _this17 = this; + var _this18 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1795,7 +1970,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data: data, requestOptions: omit(options, ['data', 'url']) }).then(action(function (res) { - _this17.fromBackend(res); + _this18.fromBackend(res); })).catch(function (e) { if (Axios.isCancel(e)) { return null; @@ -1809,14 +1984,14 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'clear', value: function clear() { - var _this18 = this; + var _this19 = this; forIn(this.__originalAttributes, function (value, key) { - _this18[key] = value; + _this19[key] = value; }); this.__activeCurrentRelations.forEach(function (currentRel) { - _this18[currentRel].clear(); + _this19[currentRel].clear(); }); } @@ -1837,13 +2012,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'hasUserChanges', get: function get() { - var _this19 = this; + var _this20 = this; if (this.__changes.length > 0) { return true; } return this.__activeCurrentRelations.some(function (rel) { - return _this19[rel].hasUserChanges; + return _this20[rel].hasUserChanges; }); } }, { @@ -2251,7 +2426,7 @@ function checkLuxonDateTime(attr, value) { } var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; -var LUXON_DATETIME_FORMAT = "yyyy'-'LL'-'dd'T'HH':'mm':'ssZZ"; +var LUXON_DATETIME_FORMAT = 'yyyy\'-\'LL\'-\'dd\'T\'HH\':\'mm\':\'ssZZ'; var CASTS = { momentDate: {