From 6f050ba5c65e492a391a1815e66241c12e89a082 Mon Sep 17 00:00:00 2001 From: Daan van der Kallen Date: Fri, 2 Apr 2021 10:48:29 +0200 Subject: [PATCH 01/41] Automatically convert files in data to the format understood by django binder --- src/BinderApi.js | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/BinderApi.js b/src/BinderApi.js index b1be24e..dcc42d5 100644 --- a/src/BinderApi.js +++ b/src/BinderApi.js @@ -1,4 +1,4 @@ -import { get } from 'lodash'; +import { get, range } from 'lodash'; import axios from 'axios'; // Function ripped from Django docs. @@ -8,6 +8,30 @@ function csrfSafeMethod(method) { return /^(GET|HEAD|OPTIONS|TRACE)$/i.test(method); } +function escapeKey(key) { + return key.toString().replace(/([.\\])/g, '\\$1'); +} + +function extractFiles(data, prefix = '') { + const keys = ( + Array.isArray(data) + ? range(data.length) + : typeof data === 'object' && data !== null + ? Object.keys(data) + : [] + ); + let files = {}; + for (const key of keys) { + if (data[key] instanceof Blob) { + files[prefix + escapeKey(key)] = data[key]; + data[key] = null; + } else if (typeof data[key] === 'object' && data[key] !== null) { + Object.assign(files, extractFiles(data[key], prefix + escapeKey(key) + '.')); + } + } + return files; +} + export default class BinderApi { baseUrl = null; csrfToken = null; @@ -63,10 +87,26 @@ export default class BinderApi { 'X-Csrftoken': useCsrfToken, }, this.defaultHeaders, - options.headers + options.headers, ); axiosOptions.headers = headers; + if ( + axiosOptions.data && + !(axiosOptions.data instanceof Blob) && + !(axiosOptions.data instanceof FormData) + ) { + const files = extractFiles(axiosOptions.data); + if (Object.keys(files).length > 0) { + const data = new FormData(); + data.append('data', JSON.stringify(axiosOptions.data)); + for (const [path, file] of Object.entries(files)) { + data.append('file:' + path, file, file.name); + } + axiosOptions.data = data; + } + } + const xhr = this.axios(axiosOptions); // We fork the promise tree as we want to have the error traverse to the listeners From bf6407aa576fbd7f41bde32868b078d3faa9a66b Mon Sep 17 00:00:00 2001 From: robinz Date: Mon, 28 Jun 2021 11:36:44 +0200 Subject: [PATCH 02/41] New models will get a negative id by default instead of null, with updated tests --- package.json | 3 +- src/Model.js | 11 +++- src/__tests__/Model.js | 78 ++++++++--------------- src/__tests__/Store.js | 24 +++++++ src/__tests__/__snapshots__/Model.js.snap | 6 +- src/__tests__/{ => helpers}/helpers.js | 0 6 files changed, 63 insertions(+), 59 deletions(-) rename src/__tests__/{ => helpers}/helpers.js (100%) 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/" ] } } diff --git a/src/Model.js b/src/Model.js index 89c3683..fbc692e 100644 --- a/src/Model.js +++ b/src/Model.js @@ -121,8 +121,9 @@ export default class Model { } /** - * 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. + * Gives the model the internal id, meaning that it will keep the set id of the model or it will receive a negative + * id if the id is null. 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() @@ -142,7 +143,7 @@ export default class Model { /** * 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 + * @returns {boolean} - True if the model id is not set or a negative integer */ @computed get isNew() { @@ -212,6 +213,10 @@ export default class Model { if (options.relations) { this.__parseRelations(options.relations); } + + // The model will automatically be assigned a negative id, the id will still be overridden if it is supplied in the data + this.assignInternalId() + if (data) { this.parse(data); } diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index a766e46..a01d945 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, @@ -63,7 +63,7 @@ test('Initialize model with invalid data', () => { test('Initialize model without data', () => { const animal = new Animal(null); - expect(animal.id).toBeNull(); + expect(animal.id).toBeLessThan(0); expect(animal.name).toBe(''); }); @@ -449,7 +449,7 @@ test('toBackend with relations', () => { id: 4, name: 'Donkey', kind: 8, - owner: null, + owner: -3, }); }); @@ -822,6 +822,7 @@ test('toBackend with observable array', () => { expect(animal.toBackend()).toEqual({ foo: ['q', 'a'], + id: -1, }); }); @@ -1150,7 +1151,7 @@ describe('requests', () => { mock.onAny().replyOnce(config => { expect(config.url).toBe('/api/animal/'); expect(config.method).toBe('post'); - expect(config.data).toBe('{"id":null,"name":"Doggo"}'); + expect(config.data).toBe('{"id":-1,"name":"Doggo"}'); return [201, { id: 10, name: 'Doggo' }]; }); @@ -1170,12 +1171,12 @@ describe('requests', () => { 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"}'); + expect(config.data).toBe('{"id":-1,"name":"Doggo"}'); return [201, { id: 10, name: 'Doggo' }]; }); return animal.validate().then(() => { - expect(animal.id).toBe(null); + expect(animal.id).toBe(-1); expect(spy).not.toHaveBeenCalled(); spy.mockReset(); @@ -1287,7 +1288,7 @@ describe('requests', () => { test('save with custom data', () => { const animal = new Animal(); mock.onAny().replyOnce(config => { - expect(JSON.parse(config.data)).toEqual({ id: null, name: '', extra_data: 'can be saved' }); + expect(JSON.parse(config.data)).toEqual({ id: -1, name: '', extra_data: 'can be saved' }); return [201, {}]; }); @@ -1968,7 +1969,7 @@ describe('changes', () => { id: 1, name: 'Lino', kind: 2, - owner: null, + owner: -4, past_owners: [5] }], relations: { @@ -2425,7 +2426,7 @@ describe('copy with changes', () => { id: 1, name: 'Lino', kind: 2, - owner: null, + owner: -4, past_owners: [5] }], relations: { @@ -2433,13 +2434,13 @@ describe('copy with changes', () => { { 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, + breed: -3, name: '', }, ], breed: [ { - id: index === 0 ? -8 : -13, + id: -3 , name: 'Cat', }, ], @@ -2500,46 +2501,19 @@ describe('copy with changes', () => { }); }); -// 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})) -// }); +test('New model instance should have a negative id instead of null', () => { + const animal = new Animal(); + expect(animal.id).toBeLessThan(0); +}); + +test('New model instance should have a null id instead of when supplied in data', () => { + const animal = new Animal({id: null}); + expect(animal.id).toBeNull(); +}); + +test('New model instance should not have negative id if a positive id was supplied in data', () => { + const animal = new Animal({id: 5}); + expect(animal.id).toBe(5); +}); diff --git a/src/__tests__/Store.js b/src/__tests__/Store.js index 397a8a8..fa0810a 100644 --- a/src/__tests__/Store.js +++ b/src/__tests__/Store.js @@ -1010,3 +1010,27 @@ describe('Pagination', () => { }); }); }); + +test('New model after adding should have a negative id when not supplied', () => { + const animalStore = new AnimalStore( ); + animalStore.add({ name: 'Cee' }); + + expect(animalStore.at(0).id).toBeLessThan(0); +}); + +test('New model after adding should have the supplied id', () => { + const animalStore = new AnimalStore( ); + animalStore.add({ id: 12, name: 'Cee' }); + + expect(animalStore.at(0).id).toBe(12); +}); + +test('Adding multiple models should get different ids', () => { + const animalStore = new AnimalStore( ); + animalStore.add({ name: 'Cee' }); + animalStore.add({ name: 'Bee' }); + + expect(animalStore.at(0).id).toBeLessThan(0); + expect(animalStore.at(1).id).toBeLessThan(0); + expect(animalStore.at(0).id).not.toBe(animalStore.at(1).id); +}); diff --git a/src/__tests__/__snapshots__/Model.js.snap b/src/__tests__/__snapshots__/Model.js.snap index 971893e..a17c65d 100644 --- a/src/__tests__/__snapshots__/Model.js.snap +++ b/src/__tests__/__snapshots__/Model.js.snap @@ -198,7 +198,7 @@ Object { "data": Array [ Object { "id": -1, - "kind": null, + "kind": -2, "name": "Doggo", "owner": -3, }, @@ -208,7 +208,7 @@ Object { Object { "id": -3, "name": "Henk", - "town": null, + "town": -4, }, ], }, @@ -254,7 +254,7 @@ Object { "id": 4, "kind": 5, "name": "", - "owner": null, + "owner": -4, }, ], "relations": Object {}, 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 849c0cd6bd1c2a20f7a7d1253f7ea12039932254 Mon Sep 17 00:00:00 2001 From: robinz Date: Mon, 28 Jun 2021 11:54:56 +0200 Subject: [PATCH 03/41] v0.28.3 --- dist/mobx-spine.cjs.js | 11 ++++++++--- dist/mobx-spine.es.js | 11 ++++++++--- package.json | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 7974f87..b0d5c0e 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -873,8 +873,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } /** - * 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. + * Gives the model the internal id, meaning that it will keep the set id of the model or it will receive a negative + * id if the id is null. 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. */ }, { @@ -925,7 +926,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {boolean} - True if the model id is not set or a negative integer */ }, { @@ -997,6 +998,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (options.relations) { this.__parseRelations(options.relations); } + + // The model will automatically be assigned a negative id, the id will still be overridden if it is supplied in the data + this.assignInternalId(); + if (data) { this.parse(data); } diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index cea489f..ea6dee0 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -867,8 +867,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } /** - * 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. + * Gives the model the internal id, meaning that it will keep the set id of the model or it will receive a negative + * id if the id is null. 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. */ }, { @@ -919,7 +920,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {boolean} - True if the model id is not set or a negative integer */ }, { @@ -991,6 +992,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (options.relations) { this.__parseRelations(options.relations); } + + // The model will automatically be assigned a negative id, the id will still be overridden if it is supplied in the data + this.assignInternalId(); + if (data) { this.parse(data); } 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 744cb651876ef6d2ba3e0011bafb964cc262feff Mon Sep 17 00:00:00 2001 From: robinz Date: Mon, 28 Jun 2021 12:07:03 +0200 Subject: [PATCH 04/41] v0.28.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0fb001d1885680d3f5b6c64c4f4a5337a4b53bf1 Mon Sep 17 00:00:00 2001 From: robinz Date: Mon, 28 Jun 2021 14:15:48 +0200 Subject: [PATCH 05/41] v0.29.0 --- dist/mobx-spine.cjs.js | 11 ++++++++++- dist/mobx-spine.es.js | 11 ++++++++++- package.json | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 3487e42..787c3df 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1002,6 +1002,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // The model will automatically be assigned a negative id, the id will still be overridden if it is supplied in the data this.assignInternalId(); + // We want our id to remain negative on a clear, only if it was not created with the id set to null + // which is usually the case when the object is a related model in which case we want the id to be reset to null + if (data && data[this.constructor.primaryKey] !== null || !data) { + this.__originalAttributes[this.constructor.primaryKey] = this[this.constructor.primaryKey]; + } + if (data) { this.parse(data); } @@ -1050,7 +1056,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (RelModel.prototype instanceof Store) { return new RelModel(options); } - return new RelModel(null, options); + // If we have a related model, we want to force the related model to have id null as that means there is no model set + var newModelData = {}; + newModelData[RelModel.primaryKey] = null; + return new RelModel(newModelData, options); })); } diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 75c31e5..2f85bc4 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -996,6 +996,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { // The model will automatically be assigned a negative id, the id will still be overridden if it is supplied in the data this.assignInternalId(); + // We want our id to remain negative on a clear, only if it was not created with the id set to null + // which is usually the case when the object is a related model in which case we want the id to be reset to null + if (data && data[this.constructor.primaryKey] !== null || !data) { + this.__originalAttributes[this.constructor.primaryKey] = this[this.constructor.primaryKey]; + } + if (data) { this.parse(data); } @@ -1044,7 +1050,10 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (RelModel.prototype instanceof Store) { return new RelModel(options); } - return new RelModel(null, options); + // If we have a related model, we want to force the related model to have id null as that means there is no model set + var newModelData = {}; + newModelData[RelModel.primaryKey] = null; + return new RelModel(newModelData, options); })); } diff --git a/package.json b/package.json index 847a76e..825162a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.4", + "version": "0.29.0", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From 7dda13eea371fb0d79e5c573d70383c93a3f3d4d Mon Sep 17 00:00:00 2001 From: robinz Date: Mon, 28 Jun 2021 14:16:27 +0200 Subject: [PATCH 06/41] Related models will still get id of null to indicate relation is empty --- src/Model.js | 11 +++- src/__tests__/Model.js | 78 +++++++++++++++++------ src/__tests__/__snapshots__/Model.js.snap | 6 +- 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/Model.js b/src/Model.js index 3720603..a95b42c 100644 --- a/src/Model.js +++ b/src/Model.js @@ -217,6 +217,12 @@ export default class Model { // The model will automatically be assigned a negative id, the id will still be overridden if it is supplied in the data this.assignInternalId() + // We want our id to remain negative on a clear, only if it was not created with the id set to null + // which is usually the case when the object is a related model in which case we want the id to be reset to null + if ((data && data[this.constructor.primaryKey] !== null) || !data){ + this.__originalAttributes[this.constructor.primaryKey] = this[this.constructor.primaryKey] + } + if (data) { this.parse(data); } @@ -270,7 +276,10 @@ export default class Model { if (RelModel.prototype instanceof Store) { return new RelModel(options); } - return new RelModel(null, options); + // If we have a related model, we want to force the related model to have id null as that means there is no model set + const newModelData = {} + newModelData[RelModel.primaryKey] = null; + return new RelModel(newModelData, options); }) ); } diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index a01d945..f7a4f21 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -449,7 +449,7 @@ test('toBackend with relations', () => { id: 4, name: 'Donkey', kind: 8, - owner: -3, + owner: null, }); }); @@ -834,7 +834,7 @@ test('clear with basic attribute', () => { animal.clear(); - expect(animal.id).toBe(null); + expect(animal.id).toBeLessThan(0); expect(animal.name).toBe(''); }); @@ -1969,7 +1969,7 @@ describe('changes', () => { id: 1, name: 'Lino', kind: 2, - owner: -4, + owner: null, past_owners: [5] }], relations: { @@ -2426,7 +2426,7 @@ describe('copy with changes', () => { id: 1, name: 'Lino', kind: 2, - owner: -4, + owner: null, past_owners: [5] }], relations: { @@ -2434,13 +2434,13 @@ describe('copy with changes', () => { { id: 2, // We don't care that our other copy gets a different id, as long as they are not the same - breed: -3, + breed: index === 0 ? -8 : -13, name: '', }, ], breed: [ { - id: -3 , + id: index === 0 ? -8 : -13 , name: 'Cat', }, ], @@ -2501,19 +2501,61 @@ describe('copy with changes', () => { }); }); -test('New model instance should have a negative id instead of null', () => { - const animal = new Animal(); - expect(animal.id).toBeLessThan(0); -}); +describe('negative id instead of null', () => { -test('New model instance should have a null id instead of when supplied in data', () => { - const animal = new Animal({id: null}); - expect(animal.id).toBeNull(); -}); + test('New model instance should have a negative id instead of null', () => { + const animal = new Animal(); + expect(animal.id).toBeLessThan(0); + }); -test('New model instance should not have negative id if a positive id was supplied in data', () => { - const animal = new Animal({id: 5}); - expect(animal.id).toBe(5); -}); + test('New model instance should have a null id instead of negative when supplied in data', () => { + const animal = new Animal({ id: null }); + expect(animal.id).toBeNull(); + }); + test('New model instance should not have negative id if a positive id was supplied in data', () => { + const animal = new Animal({ id: 5 }); + expect(animal.id).toBe(5); + }); + test('New model should keep negative id on clear', () => { + const animal = new Animal(); + animal.clear(); + expect(animal.id).toBeLessThan(0); + }); + + test('New model should keep null id on clear when created with id null', () => { + const animal = new Animal({id: null}); + animal.clear(); + expect(animal.id).toBeNull(); + }); + + test('New model should keep negative id on clear, when created with an id', () => { + const animal = new Animal({id: 5}); + animal.clear(); + expect(animal.id).toBeLessThan(0); + }); + + test('Related model should get null id if not initialized', () => { + const animal = new Animal({id: 5}, {relations: ['kind']}); + + expect(animal.kind.id).toBeNull(); + }); + + test('Related model should get null id on clear', () => { + const animal = new Animal({id: 5, kind: {id: 5}}, {relations: ['kind']}); + + expect(animal.kind.id).toBe(5); + animal.clear(); + expect(animal.kind.id).toBeNull(); + }); + + test('Related model should get null id on related model clear', () => { + const animal = new Animal({id: 5, kind: {id: 5}}, {relations: ['kind']}); + + expect(animal.kind.id).toBe(5); + animal.kind.clear(); + expect(animal.kind.id).toBeNull(); + }); + +}); diff --git a/src/__tests__/__snapshots__/Model.js.snap b/src/__tests__/__snapshots__/Model.js.snap index a17c65d..971893e 100644 --- a/src/__tests__/__snapshots__/Model.js.snap +++ b/src/__tests__/__snapshots__/Model.js.snap @@ -198,7 +198,7 @@ Object { "data": Array [ Object { "id": -1, - "kind": -2, + "kind": null, "name": "Doggo", "owner": -3, }, @@ -208,7 +208,7 @@ Object { Object { "id": -3, "name": "Henk", - "town": -4, + "town": null, }, ], }, @@ -254,7 +254,7 @@ Object { "id": 4, "kind": 5, "name": "", - "owner": -4, + "owner": null, }, ], "relations": Object {}, From e27cce98a44fa3a593dc6e42867742711e68b1a4 Mon Sep 17 00:00:00 2001 From: robinz Date: Mon, 28 Jun 2021 17:37:53 +0200 Subject: [PATCH 07/41] Add more tests showcasing clearing behaviour also after copying --- src/__tests__/Model.js | 59 +++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index f7a4f21..f44e089 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -2503,46 +2503,46 @@ describe('copy with changes', () => { describe('negative id instead of null', () => { - test('New model instance should have a negative id instead of null', () => { + test('new model instance should have a negative id instead of null', () => { const animal = new Animal(); expect(animal.id).toBeLessThan(0); }); - test('New model instance should have a null id instead of negative when supplied in data', () => { + test('new model instance should have a null id instead of negative when supplied in data', () => { const animal = new Animal({ id: null }); expect(animal.id).toBeNull(); }); - test('New model instance should not have negative id if a positive id was supplied in data', () => { + test('new model instance should not have negative id if a positive id was supplied in data', () => { const animal = new Animal({ id: 5 }); expect(animal.id).toBe(5); }); - test('New model should keep negative id on clear', () => { + test('new model should keep negative id on clear', () => { const animal = new Animal(); animal.clear(); expect(animal.id).toBeLessThan(0); }); - test('New model should keep null id on clear when created with id null', () => { + test('new model should keep null id on clear when created with id null', () => { const animal = new Animal({id: null}); animal.clear(); expect(animal.id).toBeNull(); }); - test('New model should keep negative id on clear, when created with an id', () => { + test('new model should keep negative id on clear, when created with an id', () => { const animal = new Animal({id: 5}); animal.clear(); expect(animal.id).toBeLessThan(0); }); - test('Related model should get null id if not initialized', () => { + test('related model should get null id if not initialized', () => { const animal = new Animal({id: 5}, {relations: ['kind']}); expect(animal.kind.id).toBeNull(); }); - test('Related model should get null id on clear', () => { + test('related model should get null id on clear', () => { const animal = new Animal({id: 5, kind: {id: 5}}, {relations: ['kind']}); expect(animal.kind.id).toBe(5); @@ -2550,7 +2550,7 @@ describe('negative id instead of null', () => { expect(animal.kind.id).toBeNull(); }); - test('Related model should get null id on related model clear', () => { + test('related model should get null id on related model clear', () => { const animal = new Animal({id: 5, kind: {id: 5}}, {relations: ['kind']}); expect(animal.kind.id).toBe(5); @@ -2558,4 +2558,45 @@ describe('negative id instead of null', () => { expect(animal.kind.id).toBeNull(); }); + test('model initialized with null should get negative id when clearing after copy', () => { + const animal = new Animal({id: null}); + + const copiedAnimal = animal.copy() + copiedAnimal.clear(); + expect(copiedAnimal.id).toBeLessThan(0); + }); + + test('model should get negative id when clearing after copy', () => { + const animal = new Animal(); + + const copiedAnimal = animal.copy() + copiedAnimal.clear(); + expect(copiedAnimal.id).toBeLessThan(0); + }); + + test('model should get null id when clearing after copy if it is instantiated with a null id', () => { + const animal = new Animal(); + + const copiedAnimal = new Animal({id: null}); + copiedAnimal.copy(animal) + copiedAnimal.clear(); + expect(copiedAnimal.id).toBeNull(); + }); + + test('related model should get null id on clear after copy', () => { + const animal = new Animal({ id: 5, kind: { id: 5 } }, { relations: ['kind'] }); + + const copiedAnimal = animal.copy() + copiedAnimal.clear(); + expect(copiedAnimal.kind.id).toBeNull(); + }); + + test('copying a related model should get a negative id when clear() is called on copied model', () => { + const animal = new Animal({ id: 5, kind: { id: 5 } }, { relations: ['kind'] }); + + const copiedKind = animal.kind.copy() + copiedKind.clear(); + expect(copiedKind.id).toBeLessThan(0); + }); + }); From 6ad90ae34a1f9817ced17303b01dc3bacc5bf64b Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 2 Jul 2021 17:44:06 +0200 Subject: [PATCH 08/41] v0.28.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 87c1df73e4956061541a1401ebba52be904d653a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 7 Jul 2021 13:47:11 +0200 Subject: [PATCH 09/41] v0.29.1 Ref T32139 --- dist/mobx-spine.cjs.js | 8 +++++++- dist/mobx-spine.es.js | 8 +++++++- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 8f628a2..b96b6f2 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -1924,7 +1924,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var _this19 = this; lodash.forIn(this.__originalAttributes, function (value, key) { - _this19[key] = value; + // If it is our primary key, and the primary key is negative, we generate a new negative pk, else we set it + // to the value + if (key === _this19.constructor.primaryKey && value < 0) { + _this19[key] = -1 * lodash.uniqueId(); + } else { + _this19[key] = value; + } }); this.__activeCurrentRelations.forEach(function (currentRel) { diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index c67dc3a..44c0ddd 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1918,7 +1918,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { var _this19 = this; forIn(this.__originalAttributes, function (value, key) { - _this19[key] = value; + // If it is our primary key, and the primary key is negative, we generate a new negative pk, else we set it + // to the value + if (key === _this19.constructor.primaryKey && value < 0) { + _this19[key] = -1 * uniqueId(); + } else { + _this19[key] = value; + } }); this.__activeCurrentRelations.forEach(function (currentRel) { diff --git a/package.json b/package.json index 825162a..8e82490 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.29.0", + "version": "0.29.1", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From 5f908236fcce969eb48be2a909ae3e6d68818314 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 7 Jul 2021 13:50:16 +0200 Subject: [PATCH 10/41] Create new negative id on clear Ref T32139 --- src/Model.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Model.js b/src/Model.js index cb99031..fb43343 100644 --- a/src/Model.js +++ b/src/Model.js @@ -1110,7 +1110,13 @@ export default class Model { @action clear() { forIn(this.__originalAttributes, (value, key) => { - this[key] = value; + // If it is our primary key, and the primary key is negative, we generate a new negative pk, else we set it + // to the value + if (key === this.constructor.primaryKey && value < 0){ + this[key] = -1 * uniqueId(); + } else { + this[key] = value; + } }); this.__activeCurrentRelations.forEach(currentRel => { From 14eed14a6f25c32d0ce468173903a1df5f32d9c5 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 26 Jul 2021 10:14:56 +0200 Subject: [PATCH 11/41] Fix review comments from Daan Ref T32139 --- src/Model.js | 4 +--- src/__tests__/Model.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Model.js b/src/Model.js index fb43343..dc4c61d 100644 --- a/src/Model.js +++ b/src/Model.js @@ -277,9 +277,7 @@ export default class Model { return new RelModel(options); } // If we have a related model, we want to force the related model to have id null as that means there is no model set - const newModelData = {} - newModelData[RelModel.primaryKey] = null; - return new RelModel(newModelData, options); + return new RelModel({ [RelModel.primaryKey]: null }, options); }) ); } diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index f653a5d..a9b43b2 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -2450,7 +2450,7 @@ describe('copy with changes', () => { ], breed: [ { - id: index === 0 ? -8 : -13 , + id: index === 0 ? -8 : -13, name: 'Cat', }, ], From c125b0e126383c90bedec6f50857ef6365e3c1f1 Mon Sep 17 00:00:00 2001 From: Daan van der Kallen Date: Mon, 26 Jul 2021 10:48:13 +0200 Subject: [PATCH 12/41] Test in jsdom environment so that we can use Blob/FormData --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48bda4c..b42d882 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "moment": "^2.22.0" }, "jest": { - "testEnvironment": "node", + "testEnvironment": "jsdom", "roots": [ "./src" ], From 725c2cf337bdc76d69f54531aedaf5712f979608 Mon Sep 17 00:00:00 2001 From: Daan van der Kallen Date: Mon, 26 Jul 2021 11:20:05 +0200 Subject: [PATCH 13/41] Add tests --- src/__tests__/BinderApi.js | 80 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/__tests__/BinderApi.js b/src/__tests__/BinderApi.js index a20c862..4388bae 100644 --- a/src/__tests__/BinderApi.js +++ b/src/__tests__/BinderApi.js @@ -269,3 +269,83 @@ test('Failing request with onRequestError and skipRequestError option', () => { expect(api.onRequestError).not.toHaveBeenCalled(); }); }); + +test('Blobs in json get converted to form data', () => { + const foo = new Blob(['foo'], { type: 'text/plain' }); + const bar = new Blob(['bar'], { type: 'text/plain' }); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/test/'); + expect(config.method).toBe('put'); + expect(config.params).toEqual(undefined); + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual(['data', 'file:bar.2', 'file:foo']); + + const data = JSON.parse(config.data.get('data')); + expect(data).toEqual({ + foo: null, + bar: [1, 'test', null], + }); + + const foo = config.data.get('file:foo'); + expect(foo).toBeInstanceOf(Blob); + + const bar = config.data.get('file:bar.2'); + expect(bar).toBeInstanceOf(Blob); + + return [200, {}]; + }); + + const api = new BinderApi(); + return api.put('/api/test/', { + foo, + bar: [1, 'test', bar], + }); +}); + +test('FormData is left intact', () => { + const foo = new Blob(['foo'], { type: 'text/plain' }); + const bar = new Blob(['bar'], { type: 'text/plain' }); + const data = new FormData(); + data.set('foo', foo); + data.set('bar', bar); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/test/'); + expect(config.method).toBe('put'); + expect(config.params).toEqual(undefined); + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual(['bar', 'foo']); + + const foo = config.data.get('foo'); + expect(foo).toBeInstanceOf(Blob); + + const bar = config.data.get('bar'); + expect(bar).toBeInstanceOf(Blob); + + return [200, {}]; + }); + + const api = new BinderApi(); + return api.put('/api/test/', data); +}); + +test('Blob is left intact', () => { + const data = new Blob(['foo'], { type: 'text/plain' }); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/test/'); + expect(config.method).toBe('put'); + expect(config.params).toEqual(undefined); + expect(config.data).toBeInstanceOf(Blob); + + return [200, {}]; + }); + + const api = new BinderApi(); + return api.put('/api/test/', data); +}); From be18b90f4559d575aff4b0b5b902af5ecab0be23 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 26 Jul 2021 16:24:59 +0200 Subject: [PATCH 14/41] Add documentation Ref T32139 --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a9ee7e8..1efd8b6 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ import { Model } from 'mobx-spine'; // Define a class Animal, with 2 observed properties `id` and `name`. class Animal extends Model { - @observable id = null; // Default value is null. + @observable id; // Default value is undefined, a new negative integer will automatically be generated for a new model. @observable name = ''; // Default value is ''. } ``` @@ -56,7 +56,7 @@ If we instantiate a new animal without arguments it will create an empty animal // Create an empty instance of an Animal. const lion = new Animal(); -console.log(lion.id); // null +console.log(lion.id); // -1, a unique negative integer for this model console.log(lion.name); // '' ``` @@ -67,6 +67,7 @@ You can also supply data when creating a new instance: const cat = new Animal({ id: 1, name: 'Cat' }); console.log(cat.name); // Cat +console.log(cat.id); // 1 ``` When data is supplied in the constructor, these can be reset by calling `clear`: @@ -90,6 +91,11 @@ cat.name = ''; console.log(cat.undefinedProperty); // undefined ``` +### New models +A new model is a model that exists in the store, but not on the backend. A new model either has a negative id, or the id is `null`. Checking if a model is new can be done with `model.isNew()` which returns a boolean `true` when the model is new. + +By default, a new model will be initialized with a negative id, this way the model can be used as a related model. When a model is initialized with a negative id it will automatically generate a new negative id for the model on a clear. In some cases a `null` id might be preferred, in this case a model can be forced to get a `null` id by passing `{id: null}` in the constructor. In this case the id will also be reset to `null` on a clear. A model with a `null` id functions the same as a model with a negative id other than that the model cannot be used in a relation. + ### Constructor: options |key|default| | | @@ -308,6 +314,9 @@ class animal = new Animal({ id: 2, name: 'Rova', breed: { id: 3, name: 'Main Coo console.log(animal.breed.name); // Throws cannot read property name from undefined. ``` +### Negative IDs for related models +A related model will always be initialized with a `null` id. When the id of the related model is set to `null` it indicates to django-binder that the field is empty. + ### Pick fields You can pick fields by either defining a static `pickFields` variable or a `pickFields` function. Keep in mind that `id` is mandatory, so it will always be included. From ca5532f40e26f13b879ba63842335e2f94ebee1f Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 26 Jul 2021 16:32:19 +0200 Subject: [PATCH 15/41] fix docs Ref T32139 --- dist/mobx-spine.cjs.js | 10 +++++----- dist/mobx-spine.es.js | 10 +++++----- src/Model.js | 10 +++++----- src/__tests__/helpers/helpers.js | 8 ++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index b96b6f2..715c7b8 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -860,7 +860,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {*} the id of a model or a negative id if the id is not set */ }, { @@ -888,7 +888,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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 + * @returns {string} the url for a model */ }, { @@ -926,7 +926,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {boolean} True if the model id is not set or a negative integer */ }, { @@ -1214,8 +1214,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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} + * @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} */ }, { diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 44c0ddd..6312e9f 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -854,7 +854,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {*} the id of a model or a negative id if the id is not set */ }, { @@ -882,7 +882,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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 + * @returns {string} the url for a model */ }, { @@ -920,7 +920,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {boolean} True if the model id is not set or a negative integer */ }, { @@ -1208,8 +1208,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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} + * @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} */ }, { diff --git a/src/Model.js b/src/Model.js index dc4c61d..f356195 100644 --- a/src/Model.js +++ b/src/Model.js @@ -111,7 +111,7 @@ export default class Model { /** * 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 + * @returns {*} the id of a model or a negative id if the id is not set */ getInternalId() { if (!this[this.constructor.primaryKey]) { @@ -133,7 +133,7 @@ export default class Model { * 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 + * @returns {string} the url for a model */ @computed get url() { @@ -143,7 +143,7 @@ export default class Model { /** * 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 + * @returns {boolean} True if the model id is not set or a negative integer */ @computed get isNew() { @@ -454,8 +454,8 @@ export default class 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} + * @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= undefined, options = {copyChanges: true}){ let copiedModel; diff --git a/src/__tests__/helpers/helpers.js b/src/__tests__/helpers/helpers.js index f580ee6..81a7324 100644 --- a/src/__tests__/helpers/helpers.js +++ b/src/__tests__/helpers/helpers.js @@ -42,10 +42,10 @@ 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) + * @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 From 66b1c14218ee63b6e5e3308d1a881db9fae556e2 Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Mon, 23 Aug 2021 14:48:07 +0200 Subject: [PATCH 16/41] add tests to check if files convert after model.save --- src/__tests__/Model.js | 72 +++++++++++++++++++++++++++++++- src/__tests__/fixtures/Animal.js | 26 ++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index 46bb8dc..a46371f 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -2,7 +2,7 @@ import axios from 'axios'; import { toJS, observable } from 'mobx'; import MockAdapter from 'axios-mock-adapter'; import _ from 'lodash'; -import { Model, BinderApi, Casts } from '../'; +import { Model, BinderApi } from '../'; import { Animal, AnimalStore, @@ -19,6 +19,8 @@ import { Person, PersonStore, Location, + File, + FileCabinet, } from './fixtures/Animal'; import { Customer, Location as CLocation } from './fixtures/Customer'; import animalKindBreedData from './fixtures/animal-with-kind-breed.json'; @@ -1026,6 +1028,74 @@ describe('requests', () => { }); }); + test('Save model with file', () => { + const file = new File({ id: 5 }); + const dataFile = new Blob(['foo'], { type: 'text/plain' }); + file.setInput('dataFile', dataFile); + + mock.onAny().replyOnce(config => { + expect(config.method).toBe('patch'); + + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual(['data', 'file:data_file']); + + const data = JSON.parse(config.data.get('data')); + expect(data).toEqual({ + data_file: null, + id: 5, + }); + return [200, { data: { id: 5, data_file: dataFile } }]; + }); + + file.save().then(() => { + expect(file.id).toBe(5); + expect(file.dataFile).toBeInstanceOf(Blob); + }); + }); + + test('Save model with realtion and multiple files', () => { + const fileCabinet = new FileCabinet({ id: 5 },{relations: ['files']}); + const dataFile1 = new Blob(['foo'], { type: 'text/plain' }); + fileCabinet.files.add({ dataFile: dataFile1 }); + const dataFile2 = new Blob(['bar'], { type: 'text/plain' }); + fileCabinet.files.add({ dataFile: dataFile2 }); + const dataFile3 = new Blob(['baz'], { type: 'text/plain' }); + fileCabinet.files.add({ dataFile: dataFile3 }); + + mock.onAny().replyOnce(config => { + expect(config.method).toBe('put'); + + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual([ + 'data', + 'file:with.files.0.data_file', + 'file:with.files.1.data_file', + 'file:with.files.2.data_file']); + + const data = JSON.parse(config.data.get('data')); + expect(data).toEqual({ + data: [{ + id: 5, + files: [-2, -3, -4], + }], + with: { + files: [ + { id: -2, data_file: null }, + { id: -3, data_file: null }, + { id: -4, data_file: null }, + ], + }, + }); + return [200, {}]; + }); + + fileCabinet.save({ relations: ['files'] }); + }); + test('fetch with relations', () => { const animal = new Animal( { id: 2 }, diff --git a/src/__tests__/fixtures/Animal.js b/src/__tests__/fixtures/Animal.js index b475652..d66e182 100644 --- a/src/__tests__/fixtures/Animal.js +++ b/src/__tests__/fixtures/Animal.js @@ -7,6 +7,32 @@ export class Location extends Model { @observable name = ''; } +export class File extends Model { + urlRoot = '/api/file/'; + api = new BinderApi(); + static backendResourceName = 'file'; + @observable id = null; + @observable dataFile = null; +} + +export class FileStore extends Store { + Model = File +} + +export class FileCabinet extends Model { + urlRoot = '/api/file_cabinet/'; + api = new BinderApi(); + static backendResourceName = 'file_cabinet'; + @observable id = null; + + relations() { + return { + files: FileStore, + } + } +} + + export class Breed extends Model { @observable id = null; @observable name = ''; From fbe2e80b1b72e279caa76807b936de3b82ef9d50 Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Tue, 24 Aug 2021 09:56:44 +0200 Subject: [PATCH 17/41] Improve test --- src/__tests__/Model.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index a46371f..a21f5fc 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1046,23 +1046,23 @@ describe('requests', () => { data_file: null, id: 5, }); - return [200, { data: { id: 5, data_file: dataFile } }]; + return [200, { id: 5, data_file: '/api/dataFile' } ]; }); - file.save().then(() => { + file.save().then(res => { expect(file.id).toBe(5); - expect(file.dataFile).toBeInstanceOf(Blob); + expect(file.dataFile).toBe('/api/dataFile'); }); }); - test('Save model with realtion and multiple files', () => { + test('Save model with relations and multiple files', () => { const fileCabinet = new FileCabinet({ id: 5 },{relations: ['files']}); - const dataFile1 = new Blob(['foo'], { type: 'text/plain' }); - fileCabinet.files.add({ dataFile: dataFile1 }); - const dataFile2 = new Blob(['bar'], { type: 'text/plain' }); - fileCabinet.files.add({ dataFile: dataFile2 }); - const dataFile3 = new Blob(['baz'], { type: 'text/plain' }); - fileCabinet.files.add({ dataFile: dataFile3 }); + fileCabinet.files.add([ + { dataFile: new Blob(['bar'], { type: 'text/plain' }) }, + { dataFile: new Blob(['foo'], { type: 'text/plain' }) }, + { dataFile: new Blob(['baz'], { type: 'text/plain' }) }, + ]); + mock.onAny().replyOnce(config => { expect(config.method).toBe('put'); From a2ca2ac5395f7ecbf25bb27ef8bebc3123a85e83 Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Tue, 24 Aug 2021 09:58:23 +0200 Subject: [PATCH 18/41] Improve both tests --- src/__tests__/Model.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index a21f5fc..9df36b3 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1049,7 +1049,7 @@ describe('requests', () => { return [200, { id: 5, data_file: '/api/dataFile' } ]; }); - file.save().then(res => { + file.save().then(() => { expect(file.id).toBe(5); expect(file.dataFile).toBe('/api/dataFile'); }); @@ -1063,7 +1063,6 @@ describe('requests', () => { { dataFile: new Blob(['baz'], { type: 'text/plain' }) }, ]); - mock.onAny().replyOnce(config => { expect(config.method).toBe('put'); From d7ff52ee1367b9c29f1689258b74b324f7c2cfc0 Mon Sep 17 00:00:00 2001 From: Daan van der Kallen Date: Fri, 2 Apr 2021 10:48:29 +0200 Subject: [PATCH 19/41] Automatically convert files in data to the format understood by django binder --- src/BinderApi.js | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/BinderApi.js b/src/BinderApi.js index b1be24e..dcc42d5 100644 --- a/src/BinderApi.js +++ b/src/BinderApi.js @@ -1,4 +1,4 @@ -import { get } from 'lodash'; +import { get, range } from 'lodash'; import axios from 'axios'; // Function ripped from Django docs. @@ -8,6 +8,30 @@ function csrfSafeMethod(method) { return /^(GET|HEAD|OPTIONS|TRACE)$/i.test(method); } +function escapeKey(key) { + return key.toString().replace(/([.\\])/g, '\\$1'); +} + +function extractFiles(data, prefix = '') { + const keys = ( + Array.isArray(data) + ? range(data.length) + : typeof data === 'object' && data !== null + ? Object.keys(data) + : [] + ); + let files = {}; + for (const key of keys) { + if (data[key] instanceof Blob) { + files[prefix + escapeKey(key)] = data[key]; + data[key] = null; + } else if (typeof data[key] === 'object' && data[key] !== null) { + Object.assign(files, extractFiles(data[key], prefix + escapeKey(key) + '.')); + } + } + return files; +} + export default class BinderApi { baseUrl = null; csrfToken = null; @@ -63,10 +87,26 @@ export default class BinderApi { 'X-Csrftoken': useCsrfToken, }, this.defaultHeaders, - options.headers + options.headers, ); axiosOptions.headers = headers; + if ( + axiosOptions.data && + !(axiosOptions.data instanceof Blob) && + !(axiosOptions.data instanceof FormData) + ) { + const files = extractFiles(axiosOptions.data); + if (Object.keys(files).length > 0) { + const data = new FormData(); + data.append('data', JSON.stringify(axiosOptions.data)); + for (const [path, file] of Object.entries(files)) { + data.append('file:' + path, file, file.name); + } + axiosOptions.data = data; + } + } + const xhr = this.axios(axiosOptions); // We fork the promise tree as we want to have the error traverse to the listeners From 8d5efa99063d4f00c7db88eddb0251ad5cc352b0 Mon Sep 17 00:00:00 2001 From: Daan van der Kallen Date: Mon, 26 Jul 2021 10:48:13 +0200 Subject: [PATCH 20/41] Test in jsdom environment so that we can use Blob/FormData --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f2bec7..3fadc30 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "moment": "^2.22.0" }, "jest": { - "testEnvironment": "node", + "testEnvironment": "jsdom", "roots": [ "./src" ], From b9042688726d307e310e235dd9678a9a70a9faf2 Mon Sep 17 00:00:00 2001 From: Daan van der Kallen Date: Mon, 26 Jul 2021 11:20:05 +0200 Subject: [PATCH 21/41] Add tests --- src/__tests__/BinderApi.js | 80 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/__tests__/BinderApi.js b/src/__tests__/BinderApi.js index a20c862..4388bae 100644 --- a/src/__tests__/BinderApi.js +++ b/src/__tests__/BinderApi.js @@ -269,3 +269,83 @@ test('Failing request with onRequestError and skipRequestError option', () => { expect(api.onRequestError).not.toHaveBeenCalled(); }); }); + +test('Blobs in json get converted to form data', () => { + const foo = new Blob(['foo'], { type: 'text/plain' }); + const bar = new Blob(['bar'], { type: 'text/plain' }); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/test/'); + expect(config.method).toBe('put'); + expect(config.params).toEqual(undefined); + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual(['data', 'file:bar.2', 'file:foo']); + + const data = JSON.parse(config.data.get('data')); + expect(data).toEqual({ + foo: null, + bar: [1, 'test', null], + }); + + const foo = config.data.get('file:foo'); + expect(foo).toBeInstanceOf(Blob); + + const bar = config.data.get('file:bar.2'); + expect(bar).toBeInstanceOf(Blob); + + return [200, {}]; + }); + + const api = new BinderApi(); + return api.put('/api/test/', { + foo, + bar: [1, 'test', bar], + }); +}); + +test('FormData is left intact', () => { + const foo = new Blob(['foo'], { type: 'text/plain' }); + const bar = new Blob(['bar'], { type: 'text/plain' }); + const data = new FormData(); + data.set('foo', foo); + data.set('bar', bar); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/test/'); + expect(config.method).toBe('put'); + expect(config.params).toEqual(undefined); + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual(['bar', 'foo']); + + const foo = config.data.get('foo'); + expect(foo).toBeInstanceOf(Blob); + + const bar = config.data.get('bar'); + expect(bar).toBeInstanceOf(Blob); + + return [200, {}]; + }); + + const api = new BinderApi(); + return api.put('/api/test/', data); +}); + +test('Blob is left intact', () => { + const data = new Blob(['foo'], { type: 'text/plain' }); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/test/'); + expect(config.method).toBe('put'); + expect(config.params).toEqual(undefined); + expect(config.data).toBeInstanceOf(Blob); + + return [200, {}]; + }); + + const api = new BinderApi(); + return api.put('/api/test/', data); +}); From 10160eaabe610d7ef753a9705e7423609a2ffedd Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Mon, 23 Aug 2021 14:48:07 +0200 Subject: [PATCH 22/41] add tests to check if files convert after model.save --- src/__tests__/Model.js | 72 +++++++++++++++++++++++++++++++- src/__tests__/fixtures/Animal.js | 26 ++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index 46bb8dc..a46371f 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -2,7 +2,7 @@ import axios from 'axios'; import { toJS, observable } from 'mobx'; import MockAdapter from 'axios-mock-adapter'; import _ from 'lodash'; -import { Model, BinderApi, Casts } from '../'; +import { Model, BinderApi } from '../'; import { Animal, AnimalStore, @@ -19,6 +19,8 @@ import { Person, PersonStore, Location, + File, + FileCabinet, } from './fixtures/Animal'; import { Customer, Location as CLocation } from './fixtures/Customer'; import animalKindBreedData from './fixtures/animal-with-kind-breed.json'; @@ -1026,6 +1028,74 @@ describe('requests', () => { }); }); + test('Save model with file', () => { + const file = new File({ id: 5 }); + const dataFile = new Blob(['foo'], { type: 'text/plain' }); + file.setInput('dataFile', dataFile); + + mock.onAny().replyOnce(config => { + expect(config.method).toBe('patch'); + + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual(['data', 'file:data_file']); + + const data = JSON.parse(config.data.get('data')); + expect(data).toEqual({ + data_file: null, + id: 5, + }); + return [200, { data: { id: 5, data_file: dataFile } }]; + }); + + file.save().then(() => { + expect(file.id).toBe(5); + expect(file.dataFile).toBeInstanceOf(Blob); + }); + }); + + test('Save model with realtion and multiple files', () => { + const fileCabinet = new FileCabinet({ id: 5 },{relations: ['files']}); + const dataFile1 = new Blob(['foo'], { type: 'text/plain' }); + fileCabinet.files.add({ dataFile: dataFile1 }); + const dataFile2 = new Blob(['bar'], { type: 'text/plain' }); + fileCabinet.files.add({ dataFile: dataFile2 }); + const dataFile3 = new Blob(['baz'], { type: 'text/plain' }); + fileCabinet.files.add({ dataFile: dataFile3 }); + + mock.onAny().replyOnce(config => { + expect(config.method).toBe('put'); + + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual([ + 'data', + 'file:with.files.0.data_file', + 'file:with.files.1.data_file', + 'file:with.files.2.data_file']); + + const data = JSON.parse(config.data.get('data')); + expect(data).toEqual({ + data: [{ + id: 5, + files: [-2, -3, -4], + }], + with: { + files: [ + { id: -2, data_file: null }, + { id: -3, data_file: null }, + { id: -4, data_file: null }, + ], + }, + }); + return [200, {}]; + }); + + fileCabinet.save({ relations: ['files'] }); + }); + test('fetch with relations', () => { const animal = new Animal( { id: 2 }, diff --git a/src/__tests__/fixtures/Animal.js b/src/__tests__/fixtures/Animal.js index b475652..d66e182 100644 --- a/src/__tests__/fixtures/Animal.js +++ b/src/__tests__/fixtures/Animal.js @@ -7,6 +7,32 @@ export class Location extends Model { @observable name = ''; } +export class File extends Model { + urlRoot = '/api/file/'; + api = new BinderApi(); + static backendResourceName = 'file'; + @observable id = null; + @observable dataFile = null; +} + +export class FileStore extends Store { + Model = File +} + +export class FileCabinet extends Model { + urlRoot = '/api/file_cabinet/'; + api = new BinderApi(); + static backendResourceName = 'file_cabinet'; + @observable id = null; + + relations() { + return { + files: FileStore, + } + } +} + + export class Breed extends Model { @observable id = null; @observable name = ''; From 204beaf1632299943200e0e3ac6b5ee5d2a15795 Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Tue, 24 Aug 2021 09:56:44 +0200 Subject: [PATCH 23/41] Improve test --- src/__tests__/Model.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index a46371f..a21f5fc 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1046,23 +1046,23 @@ describe('requests', () => { data_file: null, id: 5, }); - return [200, { data: { id: 5, data_file: dataFile } }]; + return [200, { id: 5, data_file: '/api/dataFile' } ]; }); - file.save().then(() => { + file.save().then(res => { expect(file.id).toBe(5); - expect(file.dataFile).toBeInstanceOf(Blob); + expect(file.dataFile).toBe('/api/dataFile'); }); }); - test('Save model with realtion and multiple files', () => { + test('Save model with relations and multiple files', () => { const fileCabinet = new FileCabinet({ id: 5 },{relations: ['files']}); - const dataFile1 = new Blob(['foo'], { type: 'text/plain' }); - fileCabinet.files.add({ dataFile: dataFile1 }); - const dataFile2 = new Blob(['bar'], { type: 'text/plain' }); - fileCabinet.files.add({ dataFile: dataFile2 }); - const dataFile3 = new Blob(['baz'], { type: 'text/plain' }); - fileCabinet.files.add({ dataFile: dataFile3 }); + fileCabinet.files.add([ + { dataFile: new Blob(['bar'], { type: 'text/plain' }) }, + { dataFile: new Blob(['foo'], { type: 'text/plain' }) }, + { dataFile: new Blob(['baz'], { type: 'text/plain' }) }, + ]); + mock.onAny().replyOnce(config => { expect(config.method).toBe('put'); From 604c335a603bc25b19c8917732e21e35e1521c6f Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Tue, 24 Aug 2021 09:58:23 +0200 Subject: [PATCH 24/41] Improve both tests --- src/__tests__/Model.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index a21f5fc..9df36b3 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1049,7 +1049,7 @@ describe('requests', () => { return [200, { id: 5, data_file: '/api/dataFile' } ]; }); - file.save().then(res => { + file.save().then(() => { expect(file.id).toBe(5); expect(file.dataFile).toBe('/api/dataFile'); }); @@ -1063,7 +1063,6 @@ describe('requests', () => { { dataFile: new Blob(['baz'], { type: 'text/plain' }) }, ]); - mock.onAny().replyOnce(config => { expect(config.method).toBe('put'); From a64c09389559d2e9a4778a1a508d1f6ab60773f9 Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Mon, 25 Oct 2021 17:18:52 +0200 Subject: [PATCH 25/41] Fix frontend_lint and update node version --- .github/workflows/ci.yml | 2 +- src/Model.js | 2 -- src/__tests__/Model.js | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42a8da4..577bca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: ['6'] + node-version: ['14'] steps: - name: Checkout code diff --git a/src/Model.js b/src/Model.js index 83bfb03..cab1363 100644 --- a/src/Model.js +++ b/src/Model.js @@ -14,7 +14,6 @@ import { mapValues, find, filter, - get, isPlainObject, isArray, omit, @@ -23,7 +22,6 @@ import { uniqBy, mapKeys, result, - pick, } from 'lodash'; import Store from './Store'; import { invariant, snakeToCamel, camelToSnake, relationsToNestedKeys, forNestedRelations } from './utils'; diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index 9df36b3..252dc25 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -518,9 +518,6 @@ test('toBackend with omit fields', () => { const serialized = model.toBackend(); - const expected = { - weight: 32 - } expect(serialized).toEqual({ color: 'red', id: 1 From 3f916bb96f6ebbd0f66bf33556aa13f7c7829be4 Mon Sep 17 00:00:00 2001 From: AB Zainuddin Date: Fri, 26 Nov 2021 12:22:38 +0100 Subject: [PATCH 26/41] v0.28.3 --- dist/mobx-spine.cjs.js | 207 ++++++++++++++++++++++++++++++++-------- dist/mobx-spine.es.js | 209 +++++++++++++++++++++++++++++++++-------- package.json | 2 +- 3 files changed, 342 insertions(+), 76 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 1592524..a94e3d6 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -79,6 +79,12 @@ function forNestedRelations(model, nestedRelations, fn) { }); } +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; +} : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; +}; + var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); @@ -144,6 +150,44 @@ var objectWithoutProperties = function (obj, keys) { return target; }; +var slicedToArray = function () { + function sliceIterator(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"]) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + return function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + return sliceIterator(arr, i); + } else { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + }; +}(); + var _class, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _class2, _temp; function _initDefineProp(target, property, descriptor, context) { @@ -1422,40 +1466,6 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function saveFiles() { return Promise.all(this.fileFields().filter(this.fieldFilter).map(this.saveFile)); } - }, { - key: 'save', - value: function save() { - var _this11 = 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) { - _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); - }); - })).catch(mobx.action(function (err) { - if (err.valErrors) { - _this11.parseValidationErrors(err.valErrors); - } - throw err; - }))); - } }, { key: 'setInput', value: function setInput(name, value) { @@ -1533,8 +1543,53 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return Promise.all(promises); } }, { - key: 'saveAll', - value: function saveAll() { + key: 'save', + value: function save() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + if (options.relations && options.relations.length > 0) { + return this._saveAll(options); + } else { + return this._save(options); + } + } + }, { + key: '_save', + value: function _save() { + var _this11 = 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) { + _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); + }); + })).catch(mobx.action(function (err) { + if (err.valErrors) { + _this11.parseValidationErrors(err.valErrors); + } + throw err; + }))); + } + }, { + key: '_saveAll', + value: function _saveAll() { var _this12 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1797,7 +1852,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { 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); +}), _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, 'setInput', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _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, '_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 @@ -1807,6 +1862,48 @@ function csrfSafeMethod(method) { ); } +function escapeKey(key) { + return key.toString().replace(/([.\\])/g, '\\$1'); +} + +function extractFiles(data) { + var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + + var keys = Array.isArray(data) ? lodash.range(data.length) : (typeof data === 'undefined' ? 'undefined' : _typeof(data)) === 'object' && data !== null ? Object.keys(data) : []; + var files = {}; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = keys[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var key = _step.value; + + if (data[key] instanceof Blob) { + files[prefix + escapeKey(key)] = data[key]; + data[key] = null; + } else if (_typeof(data[key]) === 'object' && data[key] !== null) { + Object.assign(files, extractFiles(data[key], prefix + escapeKey(key) + '.')); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return files; +} + var BinderApi = function () { function BinderApi() { classCallCheck(this, BinderApi); @@ -1861,6 +1958,42 @@ var BinderApi = function () { }, this.defaultHeaders, options.headers); axiosOptions.headers = headers; + if (axiosOptions.data && !(axiosOptions.data instanceof Blob) && !(axiosOptions.data instanceof FormData)) { + var files = extractFiles(axiosOptions.data); + if (Object.keys(files).length > 0) { + var _data = new FormData(); + _data.append('data', JSON.stringify(axiosOptions.data)); + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = Object.entries(files)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _step2$value = slicedToArray(_step2.value, 2), + path = _step2$value[0], + file = _step2$value[1]; + + _data.append('file:' + path, file, file.name); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + axiosOptions.data = _data; + } + } + var xhr = this.axios(axiosOptions); // We fork the promise tree as we want to have the error traverse to the listeners diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index e76918f..918e842 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -1,5 +1,5 @@ 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 { isArray, map, filter, find, sortBy, forIn, omit, isPlainObject, result, uniqBy, each, mapValues, uniqueId, uniq, mapKeys, get, range } from 'lodash'; import axios from 'axios'; import moment from 'moment'; import { DateTime } from 'luxon'; @@ -73,6 +73,12 @@ function forNestedRelations(model, nestedRelations, fn) { }); } +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; +} : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; +}; + var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); @@ -138,6 +144,44 @@ var objectWithoutProperties = function (obj, keys) { return target; }; +var slicedToArray = function () { + function sliceIterator(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"]) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + return function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + return sliceIterator(arr, i); + } else { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + }; +}(); + var _class, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _class2, _temp; function _initDefineProp(target, property, descriptor, context) { @@ -1416,40 +1460,6 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function saveFiles() { return Promise.all(this.fileFields().filter(this.fieldFilter).map(this.saveFile)); } - }, { - key: 'save', - value: function save() { - var _this11 = 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) { - _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); - }); - })).catch(action(function (err) { - if (err.valErrors) { - _this11.parseValidationErrors(err.valErrors); - } - throw err; - }))); - } }, { key: 'setInput', value: function setInput(name, value) { @@ -1527,8 +1537,53 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return Promise.all(promises); } }, { - key: 'saveAll', - value: function saveAll() { + key: 'save', + value: function save() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + if (options.relations && options.relations.length > 0) { + return this._saveAll(options); + } else { + return this._save(options); + } + } + }, { + key: '_save', + value: function _save() { + var _this11 = 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) { + _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); + }); + })).catch(action(function (err) { + if (err.valErrors) { + _this11.parseValidationErrors(err.valErrors); + } + throw err; + }))); + } + }, { + key: '_saveAll', + value: function _saveAll() { var _this12 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1791,7 +1846,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { 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); +}), _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, 'setInput', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_save', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_save'), _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 @@ -1801,6 +1856,48 @@ function csrfSafeMethod(method) { ); } +function escapeKey(key) { + return key.toString().replace(/([.\\])/g, '\\$1'); +} + +function extractFiles(data) { + var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + + var keys = Array.isArray(data) ? range(data.length) : (typeof data === 'undefined' ? 'undefined' : _typeof(data)) === 'object' && data !== null ? Object.keys(data) : []; + var files = {}; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = keys[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var key = _step.value; + + if (data[key] instanceof Blob) { + files[prefix + escapeKey(key)] = data[key]; + data[key] = null; + } else if (_typeof(data[key]) === 'object' && data[key] !== null) { + Object.assign(files, extractFiles(data[key], prefix + escapeKey(key) + '.')); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return files; +} + var BinderApi = function () { function BinderApi() { classCallCheck(this, BinderApi); @@ -1855,6 +1952,42 @@ var BinderApi = function () { }, this.defaultHeaders, options.headers); axiosOptions.headers = headers; + if (axiosOptions.data && !(axiosOptions.data instanceof Blob) && !(axiosOptions.data instanceof FormData)) { + var files = extractFiles(axiosOptions.data); + if (Object.keys(files).length > 0) { + var _data = new FormData(); + _data.append('data', JSON.stringify(axiosOptions.data)); + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = Object.entries(files)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _step2$value = slicedToArray(_step2.value, 2), + path = _step2$value[0], + file = _step2$value[1]; + + _data.append('file:' + path, file, file.name); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + axiosOptions.data = _data; + } + } + var xhr = this.axios(axiosOptions); // We fork the promise tree as we want to have the error traverse to the listeners diff --git a/package.json b/package.json index 3fadc30..d1cb35a 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 60f3f8b64971aaf38997fd76f60d3122bf099072 Mon Sep 17 00:00:00 2001 From: AB Zainuddin Date: Fri, 26 Nov 2021 12:23:26 +0100 Subject: [PATCH 27/41] v0.28.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1cb35a..761d8aa 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 dcb869056cbaa2fca7070f2bc67dd5ca88a225eb Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Mon, 29 Nov 2021 15:37:22 +0100 Subject: [PATCH 28/41] add check if relation is defined ref T34782 --- src/__tests__/Model.js | 19 +++++++++++++++++++ src/utils.js | 32 ++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index 252dc25..6fabaa5 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1445,6 +1445,25 @@ describe('requests', () => { return animal.save({ relations: ['kind'] }); }); + test('save all with not defined relation error', () => { + const animal = new Animal( + { id: 10, name: 'Doggo', kind: { name: 'Dog' } }, + { relations: ['kind'] } + ); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/animal/'); + expect(config.method).toBe('put'); + const putData = JSON.parse(config.data); + expect(putData).toMatchSnapshot(); + return [201, animalMultiPutResponse]; + }); + + return animal.save({ relations: ['kind', 'owner'] }); + + }); + + test('save all with empty response from backend', () => { const animal = new Animal( { name: 'Doggo', kind: { name: 'Dog' } }, diff --git a/src/utils.js b/src/utils.js index f8850a6..ec1627e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -41,22 +41,30 @@ export function relationsToNestedKeys(relations) { // Use output of relationsToNestedKeys to iterate each relation, fn is called on each model and store. export function forNestedRelations(model, nestedRelations, fn) { Object.keys(nestedRelations).forEach(key => { - if (Object.keys(nestedRelations[key]).length > 0) { - if (model[key].forEach) { - model[key].forEach(m => { - forNestedRelations(m, nestedRelations[key], fn); - }); - fn(model); - } else { - forNestedRelations(model[key], nestedRelations[key], fn); + if (!model[key]) { + + throw new Error(`Relation '${key}' is not there`); + + } else { + if (Object.keys(nestedRelations[key]).length > 0) { + if (model[key].forEach) { + model[key].forEach(m => { + forNestedRelations(m, nestedRelations[key], fn); + }); + + fn(model); + } else { + forNestedRelations(model[key], nestedRelations[key], fn); + } + } + + if (model[key].forEach) { + model[key].forEach(fn); } - } - if (model[key].forEach) { - model[key].forEach(fn); + fn(model[key]); } - fn(model[key]); }); } From 87aa1f7b4b38a6efdfc8bc28ae9cc271601e4ad3 Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Mon, 29 Nov 2021 15:42:33 +0100 Subject: [PATCH 29/41] build --- dist/mobx-spine.cjs.js | 32 +++++++++++++++++++------------- dist/mobx-spine.es.js | 32 +++++++++++++++++++------------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index a94e3d6..1ab6732 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -59,23 +59,29 @@ function relationsToNestedKeys(relations) { // 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]) { + + throw new Error('Relation \'' + key + '\' is not there'); + } else { + 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); - } + if (model[key].forEach) { + model[key].forEach(fn); + } - fn(model[key]); + fn(model[key]); + } }); } diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 918e842..d19ec58 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -53,23 +53,29 @@ function relationsToNestedKeys(relations) { // 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]) { + + throw new Error('Relation \'' + key + '\' is not there'); + } else { + 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); - } + if (model[key].forEach) { + model[key].forEach(fn); + } - fn(model[key]); + fn(model[key]); + } }); } From f7dc23f99c9ba2bc3e21353acbc7b82ee242eebf Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Mon, 29 Nov 2021 19:35:44 +0100 Subject: [PATCH 30/41] Add comment for relation check --- src/utils.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils.js b/src/utils.js index ec1627e..0cc5282 100644 --- a/src/utils.js +++ b/src/utils.js @@ -43,9 +43,8 @@ export function forNestedRelations(model, nestedRelations, fn) { Object.keys(nestedRelations).forEach(key => { if (!model[key]) { - - throw new Error(`Relation '${key}' is not there`); - + //check if passed relation is defined in relations + throw new Error(`Relation '${key}' is not defined in relations`); } else { if (Object.keys(nestedRelations[key]).length > 0) { if (model[key].forEach) { From 82ff876600c01a87f7499bc076beba6ef0471e77 Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Tue, 30 Nov 2021 11:52:20 +0100 Subject: [PATCH 31/41] Add test "save all with not defined relation error" --- src/__tests__/Model.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index 6fabaa5..e24525b 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -1459,8 +1459,10 @@ describe('requests', () => { return [201, animalMultiPutResponse]; }); - return animal.save({ relations: ['kind', 'owner'] }); - + return animal.save({ relations: ['kind', 'owner'] }).catch((e) => { + const error = 'Relation \'owner\' is not defined in relations' + expect(e.message).toEqual(error); + }) }); From fe5418cd471a5fdc4a339a8ecc53eb5ba10fdce5 Mon Sep 17 00:00:00 2001 From: Anastasiia Date: Tue, 30 Nov 2021 12:13:20 +0100 Subject: [PATCH 32/41] add snapshot --- src/__tests__/__snapshots__/Model.js.snap | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/__tests__/__snapshots__/Model.js.snap b/src/__tests__/__snapshots__/Model.js.snap index 971893e..7dd91ed 100644 --- a/src/__tests__/__snapshots__/Model.js.snap +++ b/src/__tests__/__snapshots__/Model.js.snap @@ -20,6 +20,26 @@ Object { } `; +exports[`requests save all with not defined relation error 1`] = ` +Object { + "data": Array [ + Object { + "id": 10, + "kind": -2, + "name": "Doggo", + }, + ], + "with": Object { + "kind": Array [ + Object { + "id": -2, + "name": "Dog", + }, + ], + }, +} +`; + exports[`toBackendAll should de-duplicate relations 1`] = ` Object { "data": Array [ From ee852d2ca038eb1b828ea1c7cecc14314458c771 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 28 Feb 2022 14:22:57 +0100 Subject: [PATCH 33/41] Fix styling of some comments, and added a migration guide for converting a project to use negative ids Ref T32139 --- README.md | 2 ++ dist/mobx-spine.cjs.js | 54 ++++++++++++++++++++---------------------- dist/mobx-spine.es.js | 54 ++++++++++++++++++++---------------------- src/Model.js | 10 ++++---- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 1efd8b6..b301eb5 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ A new model is a model that exists in the store, but not on the backend. A new m By default, a new model will be initialized with a negative id, this way the model can be used as a related model. When a model is initialized with a negative id it will automatically generate a new negative id for the model on a clear. In some cases a `null` id might be preferred, in this case a model can be forced to get a `null` id by passing `{id: null}` in the constructor. In this case the id will also be reset to `null` on a clear. A model with a `null` id functions the same as a model with a negative id other than that the model cannot be used in a relation. +Some projects might still use the legacy method of checking for new models by checking if `!model.id`. This does not work with the default negative IDs. To migrate a project to the default negative IDs implementation you should search and replace the whole project for occurrences of `.id` and fix all lines that check if an id is set to use the `isNew()` property. + ### Constructor: options |key|default| | | diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 715c7b8..4af4de6 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -860,7 +860,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {*} the id of a model or a negative id if the id is not set */ }, { @@ -888,7 +888,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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 + * @returns {string} the url for a model */ }, { @@ -926,7 +926,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {boolean} True if the model id is not set or a negative integer */ }, { @@ -1057,9 +1057,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return new RelModel(options); } // If we have a related model, we want to force the related model to have id null as that means there is no model set - var newModelData = {}; - newModelData[RelModel.primaryKey] = null; - return new RelModel(newModelData, options); + return new RelModel(defineProperty({}, RelModel.primaryKey, null), options); })); } @@ -1088,15 +1086,15 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function toBackend() { var _this4 = this; - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var _ref2 = 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) { + var _ref2$data = _ref2.data, + data = _ref2$data === undefined ? {} : _ref2$data, + _ref2$mapData = _ref2.mapData, + mapData = _ref2$mapData === undefined ? function (x) { return x; - } : _ref$mapData, - options = objectWithoutProperties(_ref, ['data', 'mapData']); + } : _ref2$mapData, + options = objectWithoutProperties(_ref2, ['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. @@ -1261,8 +1259,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @param source the model to copy + * @param store the store of the current model, to setChanged if there are changes * @private */ @@ -1382,14 +1380,14 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__scopeBackendResponse', - value: function __scopeBackendResponse(_ref2) { + value: function __scopeBackendResponse(_ref3) { var _this8 = this; - var data = _ref2.data, - targetRelName = _ref2.targetRelName, - repos = _ref2.repos, - mapping = _ref2.mapping, - reverseMapping = _ref2.reverseMapping; + var data = _ref3.data, + targetRelName = _ref3.targetRelName, + repos = _ref3.repos, + mapping = _ref3.mapping, + reverseMapping = _ref3.reverseMapping; var scopedData = null; var relevant = false; @@ -1455,13 +1453,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fromBackend', - value: function fromBackend(_ref3) { + value: function fromBackend(_ref4) { var _this9 = this; - var data = _ref3.data, - repos = _ref3.repos, - relMapping = _ref3.relMapping, - reverseRelMapping = _ref3.reverseRelMapping; + var data = _ref4.data, + repos = _ref4.repos, + relMapping = _ref4.relMapping, + reverseRelMapping = _ref4.reverseRelMapping; // We handle the fromBackend recursively. On each relation of the source model // fromBackend gets called as well, but with data scoped for itself @@ -1586,7 +1584,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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} + * @param options same as for a normal save request, example: {onlyChanges: true} */ }, { @@ -1722,7 +1720,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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} + * @param options same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} */ }, { diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 6312e9f..8fe1a37 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -854,7 +854,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {*} the id of a model or a negative id if the id is not set */ }, { @@ -882,7 +882,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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 + * @returns {string} the url for a model */ }, { @@ -920,7 +920,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @returns {boolean} True if the model id is not set or a negative integer */ }, { @@ -1051,9 +1051,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return new RelModel(options); } // If we have a related model, we want to force the related model to have id null as that means there is no model set - var newModelData = {}; - newModelData[RelModel.primaryKey] = null; - return new RelModel(newModelData, options); + return new RelModel(defineProperty({}, RelModel.primaryKey, null), options); })); } @@ -1082,15 +1080,15 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function toBackend() { var _this4 = this; - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var _ref2 = 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) { + var _ref2$data = _ref2.data, + data = _ref2$data === undefined ? {} : _ref2$data, + _ref2$mapData = _ref2.mapData, + mapData = _ref2$mapData === undefined ? function (x) { return x; - } : _ref$mapData, - options = objectWithoutProperties(_ref, ['data', 'mapData']); + } : _ref2$mapData, + options = objectWithoutProperties(_ref2, ['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. @@ -1255,8 +1253,8 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { /** * 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 + * @param source the model to copy + * @param store the store of the current model, to setChanged if there are changes * @private */ @@ -1376,14 +1374,14 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__scopeBackendResponse', - value: function __scopeBackendResponse(_ref2) { + value: function __scopeBackendResponse(_ref3) { var _this8 = this; - var data = _ref2.data, - targetRelName = _ref2.targetRelName, - repos = _ref2.repos, - mapping = _ref2.mapping, - reverseMapping = _ref2.reverseMapping; + var data = _ref3.data, + targetRelName = _ref3.targetRelName, + repos = _ref3.repos, + mapping = _ref3.mapping, + reverseMapping = _ref3.reverseMapping; var scopedData = null; var relevant = false; @@ -1449,13 +1447,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fromBackend', - value: function fromBackend(_ref3) { + value: function fromBackend(_ref4) { var _this9 = this; - var data = _ref3.data, - repos = _ref3.repos, - relMapping = _ref3.relMapping, - reverseRelMapping = _ref3.reverseRelMapping; + var data = _ref4.data, + repos = _ref4.repos, + relMapping = _ref4.relMapping, + reverseRelMapping = _ref4.reverseRelMapping; // We handle the fromBackend recursively. On each relation of the source model // fromBackend gets called as well, but with data scoped for itself @@ -1580,7 +1578,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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} + * @param options same as for a normal save request, example: {onlyChanges: true} */ }, { @@ -1716,7 +1714,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { * 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} + * @param options same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} */ }, { diff --git a/src/Model.js b/src/Model.js index f356195..07484bb 100644 --- a/src/Model.js +++ b/src/Model.js @@ -143,7 +143,7 @@ export default class Model { /** * 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 + * @returns {boolean} True if the model id is not set or a negative integer */ @computed get isNew() { @@ -496,8 +496,8 @@ export default class Model { /** * 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 + * @param source the model to copy + * @param store the store of the current model, to setChanged if there are changes * @private */ __copyChanges(source, store) { @@ -801,7 +801,7 @@ 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} + * @param options same as for a normal save request, example: {onlyChanges: true} */ validate(options = {}){ // Add the validate parameter @@ -914,7 +914,7 @@ export default class Model { * 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} + * @param options same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true} */ validateAll(options = {}){ // Add the validate option From 7de2476130ba86b43ca0cd7cd8a0206c7abbc187 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 28 Feb 2022 14:28:05 +0100 Subject: [PATCH 34/41] build es files Ref T32139 --- dist/mobx-spine.cjs.js | 135 +++++++++++++++++++---------------------- dist/mobx-spine.es.js | 135 +++++++++++++++++++---------------------- 2 files changed, 124 insertions(+), 146 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 4af4de6..3e04491 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -873,7 +873,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } /** - * Gives the model the internal id, meaning that it will keep the set id of the model or it will receive a negative + * Gives the model the internal id, meaning that it will keep the set id of the model or will receive a negative * id if the id is null. 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. */ @@ -1579,66 +1579,6 @@ 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 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() { - 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) { @@ -1717,30 +1657,79 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } /** - * 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} - */ + * 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: 'validateAll', - value: function validateAll() { + key: 'validate', + value: function validate() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - // Add the validate option + // Add the validate parameter if (options.params) { options.params.validate = true; } else { options.params = { validate: true }; } - return this.saveAll(options).catch(function (err) { + + return this.save(options).catch(function (err) { throw err; }); } }, { - key: 'saveAll', - value: function saveAll() { + key: 'save', + value: function save() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + if (options.relations && options.relations.length > 0) { + return this._saveAll(options); + } else { + 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: '_saveAll', + value: function _saveAll() { var _this13 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -2012,7 +2001,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { 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); +}), _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, 'setInput', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _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, '_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 diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 8fe1a37..5d02b42 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -867,7 +867,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } /** - * Gives the model the internal id, meaning that it will keep the set id of the model or it will receive a negative + * Gives the model the internal id, meaning that it will keep the set id of the model or will receive a negative * id if the id is null. 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. */ @@ -1573,66 +1573,6 @@ 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 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() { - 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) { @@ -1711,30 +1651,79 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { } /** - * 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} - */ + * 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: 'validateAll', - value: function validateAll() { + key: 'validate', + value: function validate() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - // Add the validate option + // Add the validate parameter if (options.params) { options.params.validate = true; } else { options.params = { validate: true }; } - return this.saveAll(options).catch(function (err) { + + return this.save(options).catch(function (err) { throw err; }); } }, { - key: 'saveAll', - value: function saveAll() { + key: 'save', + value: function save() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + if (options.relations && options.relations.length > 0) { + return this._saveAll(options); + } else { + 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: '_saveAll', + value: function _saveAll() { var _this13 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -2006,7 +1995,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { 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); +}), _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, 'setInput', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_save', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_save'), _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 From ccc3e1d609964254e0377dd2c940076d725d4a14 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 28 Feb 2022 14:30:12 +0100 Subject: [PATCH 35/41] v0.29.2 Ref T32139 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c30b33..a97dcb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.29.1", + "version": "0.29.2", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From 34cedfcbc865db8b225f44ffa3f96836a435ad6b Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 28 Feb 2022 14:30:28 +0100 Subject: [PATCH 36/41] v0.30.0 Ref T32139 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a97dcb8..8e43f43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.29.2", + "version": "0.30.0", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", From d7897bec09241b2aff2b4d115e71f9ebaf702ec4 Mon Sep 17 00:00:00 2001 From: Stefan Majoor Date: Thu, 3 Mar 2022 14:04:13 +0100 Subject: [PATCH 37/41] add failing test for inconsistent ordering --- src/__tests__/Model.js | 39 +++++++++++++++++++ .../fixtures/inconsistent-ordering-with.json | 32 +++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/__tests__/fixtures/inconsistent-ordering-with.json diff --git a/src/__tests__/Model.js b/src/__tests__/Model.js index e24525b..ed89d5d 100644 --- a/src/__tests__/Model.js +++ b/src/__tests__/Model.js @@ -33,6 +33,7 @@ import customerWithoutTownRestaurants from './fixtures/customer-without-town-res import customersLocationBestCookWorkPlaces from './fixtures/customers-location-best-cook-work-places.json'; import saveFailData from './fixtures/save-fail.json'; import saveNewFailData from './fixtures/save-new-fail.json'; +import inconsistentOrderingWith from './fixtures/inconsistent-ordering-with.json'; beforeEach(() => { // Refresh lodash's `_.uniqueId` internal state for every test @@ -1922,3 +1923,41 @@ describe('changes', () => { expect(animal.hasUserChanges).toBe(true); }); }); + +/** + * Test that for withs, the ordering is taken from the ids on the main model, and not in the withs. + * + * i.e. + * + * data { + * "past_owners": [ + * 55, + * 66 + * ], + * } + * with: {past_owners: [ + * {id:66}, + * {id:55} + * ]) + * + * Past owners will be sorted 55, 66, and not the other way around + * + * + */ +test('Parsing inconsistent ordering of withs', () => { + + const animal = new Animal(null, { + relations: ['pastOwners.town'], + }); + + expect(animal.pastOwners).not.toBeUndefined(); + expect(animal.pastOwners).toBeInstanceOf(PersonStore); + + animal.fromBackend({ + data: inconsistentOrderingWith.data, + repos: inconsistentOrderingWith.with, + relMapping: inconsistentOrderingWith.with_mapping, + }); + + expect(animal.pastOwners.map('id')).toEqual([55, 66]); +}); diff --git a/src/__tests__/fixtures/inconsistent-ordering-with.json b/src/__tests__/fixtures/inconsistent-ordering-with.json new file mode 100644 index 0000000..b0c981c --- /dev/null +++ b/src/__tests__/fixtures/inconsistent-ordering-with.json @@ -0,0 +1,32 @@ +{ + "data": { + "color": "Brown", + "past_owners": [ + 55, + 66 + ], + "name": "Cat", + "kind": 2, + "id": 2, + "deleted": false + }, + "with": { + "animal_owner": [ + { + "name": "Bobbie", + "id": 66, + "town": 10 + }, + { + "name": "Oessein", + "id": 55, + "town": 11 + } + ] + }, + "meta": { + }, + "with_mapping": { + "past_owners": "animal_owner" + } +} From b3855dff708f66bf75fc235764f3e191d6a185ef Mon Sep 17 00:00:00 2001 From: Stefan Majoor Date: Thu, 3 Mar 2022 14:12:52 +0100 Subject: [PATCH 38/41] fix instable ordering in relations --- src/Model.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Model.js b/src/Model.js index cab1363..1942d16 100644 --- a/src/Model.js +++ b/src/Model.js @@ -440,7 +440,8 @@ export default class Model { __parseRepositoryToData(key, repository) { if (isArray(key)) { - return filter(repository, m => key.includes(m.id)); + const models = key.map(k => find(repository, { id: k })); + return filter(models, m => m); } return find(repository, { id: key }); } From 5a66e987d1507a753701e6071eeed214045ca1bd Mon Sep 17 00:00:00 2001 From: Stefan Majoor Date: Thu, 3 Mar 2022 14:14:16 +0100 Subject: [PATCH 39/41] update build --- dist/mobx-spine.cjs.js | 11 +++++++---- dist/mobx-spine.es.js | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 1ab6732..3c60d4c 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -61,8 +61,8 @@ function forNestedRelations(model, nestedRelations, fn) { Object.keys(nestedRelations).forEach(function (key) { if (!model[key]) { - - throw new Error('Relation \'' + key + '\' is not there'); + //check if passed relation is defined in relations + throw new Error('Relation \'' + key + '\' is not defined in relations'); } else { if (Object.keys(nestedRelations[key]).length > 0) { if (model[key].forEach) { @@ -1245,8 +1245,11 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { key: '__parseRepositoryToData', value: function __parseRepositoryToData(key, repository) { if (lodash.isArray(key)) { - return lodash.filter(repository, function (m) { - return key.includes(m.id); + var models = key.map(function (k) { + return lodash.find(repository, { id: k }); + }); + return lodash.filter(models, function (m) { + return m; }); } return lodash.find(repository, { id: key }); diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index d19ec58..39d4226 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -55,8 +55,8 @@ function forNestedRelations(model, nestedRelations, fn) { Object.keys(nestedRelations).forEach(function (key) { if (!model[key]) { - - throw new Error('Relation \'' + key + '\' is not there'); + //check if passed relation is defined in relations + throw new Error('Relation \'' + key + '\' is not defined in relations'); } else { if (Object.keys(nestedRelations[key]).length > 0) { if (model[key].forEach) { @@ -1239,8 +1239,11 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { key: '__parseRepositoryToData', value: function __parseRepositoryToData(key, repository) { if (isArray(key)) { - return filter(repository, function (m) { - return key.includes(m.id); + var models = key.map(function (k) { + return find(repository, { id: k }); + }); + return filter(models, function (m) { + return m; }); } return find(repository, { id: key }); From 6e9a5fc9cb7913e55cf94451f843e4d95e7dd1b5 Mon Sep 17 00:00:00 2001 From: Daan van der Kallen Date: Fri, 4 Mar 2022 11:08:55 +0100 Subject: [PATCH 40/41] Improve running time of repository parsing --- src/Model.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Model.js b/src/Model.js index 1942d16..081195b 100644 --- a/src/Model.js +++ b/src/Model.js @@ -440,8 +440,10 @@ export default class Model { __parseRepositoryToData(key, repository) { if (isArray(key)) { - const models = key.map(k => find(repository, { id: k })); - return filter(models, m => m); + const idIndexes = Object.fromEntries(key.map((id, index) => [id, index])); + const models = repository.filter(({ id }) => idIndexes[id] !== undefined); + models.sort((l, r) => idIndexes[l.id] - idIndexes[r.id]); + return models; } return find(repository, { id: key }); } From 126db335d157237a52dac8c043e7aa197ae4c20f Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 28 Mar 2022 10:57:22 +0200 Subject: [PATCH 41/41] build es files Ref T32139 --- dist/mobx-spine.cjs.js | 400 ++++++++++++++++++++++++++++++----------- dist/mobx-spine.es.js | 400 ++++++++++++++++++++++++++++++----------- 2 files changed, 598 insertions(+), 202 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 3c60d4c..8416201 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -907,14 +907,40 @@ 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, meaning that it will keep the set id of the model or will receive a negative + * id if the id is null. 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() { @@ -945,12 +971,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', @@ -1016,6 +1048,16 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (options.relations) { this.__parseRelations(options.relations); } + + // The model will automatically be assigned a negative id, the id will still be overridden if it is supplied in the data + this.assignInternalId(); + + // We want our id to remain negative on a clear, only if it was not created with the id set to null + // which is usually the case when the object is a related model in which case we want the id to be reset to null + if (data && data[this.constructor.primaryKey] !== null || !data) { + this.__originalAttributes[this.constructor.primaryKey] = this[this.constructor.primaryKey]; + } + if (data) { this.parse(data); } @@ -1036,7 +1078,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); @@ -1054,14 +1096,18 @@ 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 }; if (RelModel.prototype instanceof Store) { return new RelModel(options); } - return new RelModel(null, options); + // If we have a related model, we want to force the related model to have id null as that means there is no model set + return new RelModel(defineProperty({}, RelModel.primaryKey, null), options); })); } @@ -1090,15 +1136,15 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function toBackend() { var _this4 = this; - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var _ref2 = 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) { + var _ref2$data = _ref2.data, + data = _ref2$data === undefined ? {} : _ref2$data, + _ref2$mapData = _ref2.mapData, + mapData = _ref2$mapData === undefined ? function (x) { return x; - } : _ref$mapData, - options = objectWithoutProperties(_ref, ['data', 'mapData']); + } : _ref2$mapData, + options = objectWithoutProperties(_ref2, ['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. @@ -1208,18 +1254,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(); } @@ -1245,12 +1402,17 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { key: '__parseRepositoryToData', value: function __parseRepositoryToData(key, repository) { if (lodash.isArray(key)) { - var models = key.map(function (k) { - return lodash.find(repository, { id: k }); + var idIndexes = Object.fromEntries(key.map(function (id, index) { + return [id, index]; + })); + var models = repository.filter(function (_ref3) { + var id = _ref3.id; + return idIndexes[id] !== undefined; }); - return lodash.filter(models, function (m) { - return m; + models.sort(function (l, r) { + return idIndexes[l.id] - idIndexes[r.id]; }); + return models; } return lodash.find(repository, { id: key }); } @@ -1276,14 +1438,14 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__scopeBackendResponse', - value: function __scopeBackendResponse(_ref2) { - var _this7 = this; + value: function __scopeBackendResponse(_ref4) { + var _this8 = this; - var data = _ref2.data, - targetRelName = _ref2.targetRelName, - repos = _ref2.repos, - mapping = _ref2.mapping, - reverseMapping = _ref2.reverseMapping; + var data = _ref4.data, + targetRelName = _ref4.targetRelName, + repos = _ref4.repos, + mapping = _ref4.mapping, + reverseMapping = _ref4.reverseMapping; var scopedData = null; var relevant = false; @@ -1299,18 +1461,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) { @@ -1349,13 +1511,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fromBackend', - value: function fromBackend(_ref3) { - var _this8 = this; + value: function fromBackend(_ref5) { + var _this9 = this; - var data = _ref3.data, - repos = _ref3.repos, - relMapping = _ref3.relMapping, - reverseRelMapping = _ref3.reverseRelMapping; + var data = _ref5.data, + repos = _ref5.repos, + relMapping = _ref5.relMapping, + reverseRelMapping = _ref5.reverseRelMapping; // We handle the fromBackend recursively. On each relation of the source model // fromBackend gets called as well, but with data scoped for itself @@ -1363,8 +1525,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, @@ -1406,22 +1568,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(); } } }); @@ -1441,7 +1603,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); @@ -1452,16 +1614,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]; @@ -1551,6 +1713,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() { @@ -1565,7 +1751,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] : {}; @@ -1581,17 +1767,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; }))); @@ -1599,7 +1788,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] : {}; @@ -1615,31 +1804,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; }))); @@ -1651,19 +1843,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); }); } @@ -1675,7 +1867,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; @@ -1688,24 +1880,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(); }); } @@ -1723,12 +1915,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(); @@ -1754,7 +1946,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] : {}; @@ -1766,7 +1958,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; @@ -1774,26 +1966,32 @@ 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; + // If it is our primary key, and the primary key is negative, we generate a new negative pk, else we set it + // to the value + if (key === _this19.constructor.primaryKey && value < 0) { + _this19[key] = -1 * lodash.uniqueId(); + } else { + _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 39d4226..8dfa982 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -901,14 +901,40 @@ 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, meaning that it will keep the set id of the model or will receive a negative + * id if the id is null. 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() { @@ -939,12 +965,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', @@ -1010,6 +1042,16 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { if (options.relations) { this.__parseRelations(options.relations); } + + // The model will automatically be assigned a negative id, the id will still be overridden if it is supplied in the data + this.assignInternalId(); + + // We want our id to remain negative on a clear, only if it was not created with the id set to null + // which is usually the case when the object is a related model in which case we want the id to be reset to null + if (data && data[this.constructor.primaryKey] !== null || !data) { + this.__originalAttributes[this.constructor.primaryKey] = this[this.constructor.primaryKey]; + } + if (data) { this.parse(data); } @@ -1030,7 +1072,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); @@ -1048,14 +1090,18 @@ 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 }; if (RelModel.prototype instanceof Store) { return new RelModel(options); } - return new RelModel(null, options); + // If we have a related model, we want to force the related model to have id null as that means there is no model set + return new RelModel(defineProperty({}, RelModel.primaryKey, null), options); })); } @@ -1084,15 +1130,15 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { value: function toBackend() { var _this4 = this; - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var _ref2 = 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) { + var _ref2$data = _ref2.data, + data = _ref2$data === undefined ? {} : _ref2$data, + _ref2$mapData = _ref2.mapData, + mapData = _ref2$mapData === undefined ? function (x) { return x; - } : _ref$mapData, - options = objectWithoutProperties(_ref, ['data', 'mapData']); + } : _ref2$mapData, + options = objectWithoutProperties(_ref2, ['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. @@ -1202,18 +1248,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$$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(); } @@ -1239,12 +1396,17 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { key: '__parseRepositoryToData', value: function __parseRepositoryToData(key, repository) { if (isArray(key)) { - var models = key.map(function (k) { - return find(repository, { id: k }); + var idIndexes = Object.fromEntries(key.map(function (id, index) { + return [id, index]; + })); + var models = repository.filter(function (_ref3) { + var id = _ref3.id; + return idIndexes[id] !== undefined; }); - return filter(models, function (m) { - return m; + models.sort(function (l, r) { + return idIndexes[l.id] - idIndexes[r.id]; }); + return models; } return find(repository, { id: key }); } @@ -1270,14 +1432,14 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__scopeBackendResponse', - value: function __scopeBackendResponse(_ref2) { - var _this7 = this; + value: function __scopeBackendResponse(_ref4) { + var _this8 = this; - var data = _ref2.data, - targetRelName = _ref2.targetRelName, - repos = _ref2.repos, - mapping = _ref2.mapping, - reverseMapping = _ref2.reverseMapping; + var data = _ref4.data, + targetRelName = _ref4.targetRelName, + repos = _ref4.repos, + mapping = _ref4.mapping, + reverseMapping = _ref4.reverseMapping; var scopedData = null; var relevant = false; @@ -1293,18 +1455,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) { @@ -1343,13 +1505,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fromBackend', - value: function fromBackend(_ref3) { - var _this8 = this; + value: function fromBackend(_ref5) { + var _this9 = this; - var data = _ref3.data, - repos = _ref3.repos, - relMapping = _ref3.relMapping, - reverseRelMapping = _ref3.reverseRelMapping; + var data = _ref5.data, + repos = _ref5.repos, + relMapping = _ref5.relMapping, + reverseRelMapping = _ref5.reverseRelMapping; // We handle the fromBackend recursively. On each relation of the source model // fromBackend gets called as well, but with data scoped for itself @@ -1357,8 +1519,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, @@ -1400,22 +1562,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(); } } }); @@ -1435,7 +1597,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); @@ -1446,16 +1608,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]; @@ -1545,6 +1707,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() { @@ -1559,7 +1745,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] : {}; @@ -1575,17 +1761,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; }))); @@ -1593,7 +1782,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] : {}; @@ -1609,31 +1798,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; }))); @@ -1645,19 +1837,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); }); } @@ -1669,7 +1861,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; @@ -1682,24 +1874,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(); }); } @@ -1717,12 +1909,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(); @@ -1748,7 +1940,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] : {}; @@ -1760,7 +1952,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; @@ -1768,26 +1960,32 @@ 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; + // If it is our primary key, and the primary key is negative, we generate a new negative pk, else we set it + // to the value + if (key === _this19.constructor.primaryKey && value < 0) { + _this19[key] = -1 * uniqueId(); + } else { + _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; }); } }, {