Skip to content

Commit dc3c38f

Browse files
author
robin
committed
Merge remote-tracking branch 'origin-cy/master' into T32139_default_negative_ids_by_default_revised # Conflicts: # dist/mobx-spine.cjs.js # dist/mobx-spine.es.js # src/Model.js # src/__tests__/Model.js
Ref T32139
2 parents 9d80547 + cec7709 commit dc3c38f

13 files changed

Lines changed: 579 additions & 292 deletions

File tree

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Continuous Integration
2+
3+
on: push
4+
5+
jobs:
6+
check:
7+
runs-on: ubuntu-latest
8+
9+
strategy:
10+
matrix:
11+
node-version: ['14']
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v2
16+
17+
- name: Setup NodeJS ${{ matrix.node-version }}
18+
uses: actions/setup-node@v2
19+
with:
20+
node-version: ${{ matrix.node-version }}
21+
cache: 'yarn'
22+
23+
- name: Install requirements
24+
run: |
25+
yarn install --frozen-lockfile
26+
27+
- name: Run tests
28+
run: |
29+
yarn ci
30+
31+
- name: Upload coverage report
32+
uses: codecov/codecov-action@v1

.travis.yml

Lines changed: 0 additions & 9 deletions
This file was deleted.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ The `save` function accepts a few paramaters as an `options` object:
222222
|data|undefined|When set, append `data` to result. Existing keys from `toBackend` will be overwritten by data, while new keys will be added. | `animal.save({ data: { id: 1, some_other_field: 'will be added' } })`
223223
|mapData|undefined|You can change the data which will be used for the request send by supplying a function. First argument is the formatted data ready for sending a request. Called at the very last of data formatting operations.| `animal.save({ mapData: data => (...data, some_other_field: 'will be added' } ) } })`
224224
|forceFields|undefined|When `onlyChanges` is given, you can force fields to be included despite of having no changes.| `animal.save({ onlyChanges: true, forceFields: ['name'] } ) } })`
225-
|relations|undefined|Relations to be instantiated when instantiating this model as well. Should be an array of strings.| `animal = new Animal({ relations: ['location', 'owner.parents'] })`
225+
|relations|undefined|Relations to save when saving this model as well. Note that its not needed to include relations here so that they will be linked, only to save the models themselves. Should be an array of strings.| `animal.save({ relations: ['location', 'owner.parents'] })`
226226

227227
#### Backend request: delete
228228

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mobx-spine",
3-
"version": "0.28.1",
3+
"version": "0.28.4",
44
"license": "ISC",
55
"author": "Kees Kluskens <kees@webduck.nl>",
66
"description": "MobX with support for models, relations and an API.",
@@ -19,7 +19,7 @@
1919
"version": "git add -A dist",
2020
"build": "rm -f dist/** && BABEL_ENV=production node build.js",
2121
"//precommit": "lint-staged",
22-
"ci": "npm run -s lint && npm run -s test-coverage && codecov"
22+
"ci": "npm run -s lint && npm run -s test-coverage"
2323
},
2424
"lint-staged": {
2525
"*.js": [
@@ -45,13 +45,13 @@
4545
"babel-plugin-transform-es2015-modules-commonjs": "6.26.2",
4646
"babel-preset-es2015": "6.24.1",
4747
"babel-preset-stage-2": "6.24.1",
48-
"codecov": "3.4.0",
48+
"codecov": "3.7.1",
4949
"eslint": "5.16.0",
5050
"eslint-config-codeyellow": "4.1.5",
5151
"husky": "0.14.3",
5252
"jest": "22.4.4",
5353
"lint-staged": "7.1.1",
54-
"lodash": "4.17.11",
54+
"lodash": "4.17.21",
5555
"luxon": "1.24.1",
5656
"mobx": "4.9.4",
5757
"moment": "2.24.0",
@@ -65,7 +65,7 @@
6565
"moment": "^2.22.0"
6666
},
6767
"jest": {
68-
"testEnvironment": "node",
68+
"testEnvironment": "jsdom",
6969
"roots": [
7070
"./src"
7171
],

src/BinderApi.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { get } from 'lodash';
1+
import { get, range } from 'lodash';
22
import axios from 'axios';
33

44
// Function ripped from Django docs.
@@ -8,6 +8,30 @@ function csrfSafeMethod(method) {
88
return /^(GET|HEAD|OPTIONS|TRACE)$/i.test(method);
99
}
1010

11+
function escapeKey(key) {
12+
return key.toString().replace(/([.\\])/g, '\\$1');
13+
}
14+
15+
function extractFiles(data, prefix = '') {
16+
const keys = (
17+
Array.isArray(data)
18+
? range(data.length)
19+
: typeof data === 'object' && data !== null
20+
? Object.keys(data)
21+
: []
22+
);
23+
let files = {};
24+
for (const key of keys) {
25+
if (data[key] instanceof Blob) {
26+
files[prefix + escapeKey(key)] = data[key];
27+
data[key] = null;
28+
} else if (typeof data[key] === 'object' && data[key] !== null) {
29+
Object.assign(files, extractFiles(data[key], prefix + escapeKey(key) + '.'));
30+
}
31+
}
32+
return files;
33+
}
34+
1135
export default class BinderApi {
1236
baseUrl = null;
1337
csrfToken = null;
@@ -63,10 +87,26 @@ export default class BinderApi {
6387
'X-Csrftoken': useCsrfToken,
6488
},
6589
this.defaultHeaders,
66-
options.headers
90+
options.headers,
6791
);
6892
axiosOptions.headers = headers;
6993

94+
if (
95+
axiosOptions.data &&
96+
!(axiosOptions.data instanceof Blob) &&
97+
!(axiosOptions.data instanceof FormData)
98+
) {
99+
const files = extractFiles(axiosOptions.data);
100+
if (Object.keys(files).length > 0) {
101+
const data = new FormData();
102+
data.append('data', JSON.stringify(axiosOptions.data));
103+
for (const [path, file] of Object.entries(files)) {
104+
data.append('file:' + path, file, file.name);
105+
}
106+
axiosOptions.data = data;
107+
}
108+
}
109+
70110
const xhr = this.axios(axiosOptions);
71111

72112
// We fork the promise tree as we want to have the error traverse to the listeners

src/Model.js

Lines changed: 60 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
mapValues,
1515
find,
1616
filter,
17-
get,
1817
isPlainObject,
1918
isArray,
2019
omit,
@@ -23,7 +22,6 @@ import {
2322
uniqBy,
2423
mapKeys,
2524
result,
26-
pick,
2725
} from 'lodash';
2826
import Store from './Store';
2927
import { invariant, snakeToCamel, camelToSnake, relationsToNestedKeys, forNestedRelations } from './utils';
@@ -577,7 +575,10 @@ export default class Model {
577575

578576
__parseRepositoryToData(key, repository) {
579577
if (isArray(key)) {
580-
return filter(repository, m => key.includes(m.id));
578+
const idIndexes = Object.fromEntries(key.map((id, index) => [id, index]));
579+
const models = repository.filter(({ id }) => idIndexes[id] !== undefined);
580+
models.sort((l, r) => idIndexes[l.id] - idIndexes[r.id]);
581+
return models;
581582
}
582583
return find(repository, { id: key });
583584
}
@@ -727,7 +728,7 @@ export default class Model {
727728
} else if (this.__activeCurrentRelations.includes(attr)) {
728729
// In Binder, a relation property is an `int` or `[int]`, referring to its ID.
729730
// However, it can also be an object if there are nested relations (non flattened).
730-
if (isPlainObject(value) || isPlainObject(get(value, '[0]'))) {
731+
if (isPlainObject(value) || (Array.isArray(value) && value.every(isPlainObject))) {
731732
this[attr].parse(value);
732733
} else if (value === null) {
733734
// The relation is cleared.
@@ -797,63 +798,6 @@ export default class Model {
797798
);
798799
}
799800

800-
/**
801-
* Validates a model by sending a save request to binder with the validate header set. Binder will return the validation
802-
* errors without actually committing the save
803-
*
804-
* @param options - same as for a normal save request, example: {onlyChanges: true}
805-
*/
806-
validate(options = {}){
807-
// Add the validate parameter
808-
if (options.params){
809-
options.params.validate = true
810-
} else {
811-
options.params = { validate: true };
812-
}
813-
return this.save(options);
814-
}
815-
816-
@action
817-
save(options = {}) {
818-
this.clearValidationErrors();
819-
return this.wrapPendingRequestCount(
820-
this.__getApi()
821-
.saveModel({
822-
url: options.url || this.url,
823-
data: this.toBackend({
824-
data: options.data,
825-
mapData: options.mapData,
826-
fields: options.fields,
827-
onlyChanges: options.onlyChanges,
828-
}),
829-
isNew: this.isNew,
830-
requestOptions: omit(options, 'url', 'data', 'mapData')
831-
})
832-
.then(action(res => {
833-
// Only update the model when we are actually trying to save
834-
if (!options.params || !options.params.validate) {
835-
this.saveFromBackend({
836-
...res,
837-
data: omit(res.data, this.fileFields().map(camelToSnake)),
838-
});
839-
this.clearUserFieldChanges();
840-
return this.saveFiles().then(() => {
841-
this.clearUserFileChanges();
842-
return Promise.resolve(res);
843-
});
844-
}
845-
}))
846-
.catch(
847-
action(err => {
848-
if (err.valErrors) {
849-
this.parseValidationErrors(err.valErrors);
850-
}
851-
throw err;
852-
})
853-
)
854-
);
855-
}
856-
857801
@action
858802
setInput(name, value) {
859803
invariant(
@@ -911,23 +855,72 @@ export default class Model {
911855
}
912856

913857
/**
914-
* Validates a model and relations by sending a save request to binder with the validate header set. Binder will return the validation
858+
* Validates a model by sending a save request to binder with the validate header set. Binder will return the validation
915859
* errors without actually committing the save
916860
*
917-
* @param options - same as for a normal saveAll request, example {relations:['foo'], onlyChanges: true}
861+
* @param options - same as for a normal save request, example: {onlyChanges: true}
918862
*/
919-
validateAll(options = {}){
920-
// Add the validate option
863+
validate(options = {}){
864+
// Add the validate parameter
921865
if (options.params){
922866
options.params.validate = true
923867
} else {
924868
options.params = { validate: true };
925869
}
926-
return this.saveAll(options);
870+
return this.save(options);
871+
}
872+
873+
save(options = {}) {
874+
if (options.relations && options.relations.length > 0) {
875+
return this._saveAll(options);
876+
} else {
877+
return this._save(options);
878+
}
879+
}
880+
881+
@action
882+
_save(options = {}) {
883+
this.clearValidationErrors();
884+
return this.wrapPendingRequestCount(
885+
this.__getApi()
886+
.saveModel({
887+
url: options.url || this.url,
888+
data: this.toBackend({
889+
data: options.data,
890+
mapData: options.mapData,
891+
fields: options.fields,
892+
onlyChanges: options.onlyChanges,
893+
}),
894+
isNew: this.isNew,
895+
requestOptions: omit(options, 'url', 'data', 'mapData')
896+
})
897+
.then(action(res => {
898+
// Only update the models if we are actually trying to save
899+
if (!options.params || !options.params.validate) {
900+
this.saveFromBackend({
901+
...res,
902+
data: omit(res.data, this.fileFields().map(camelToSnake)),
903+
});
904+
this.clearUserFieldChanges();
905+
return this.saveFiles().then(() => {
906+
this.clearUserFileChanges();
907+
return Promise.resolve(res);
908+
});
909+
}
910+
}))
911+
.catch(
912+
action(err => {
913+
if (err.valErrors) {
914+
this.parseValidationErrors(err.valErrors);
915+
}
916+
throw err;
917+
})
918+
)
919+
);
927920
}
928921

929922
@action
930-
saveAll(options = {}) {
923+
_saveAll(options = {}) {
931924
this.clearValidationErrors();
932925
return this.wrapPendingRequestCount(
933926
this.__getApi()

0 commit comments

Comments
 (0)