Skip to content

Commit

Permalink
Add different way to define relations on models
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanmajoor committed Nov 22, 2023
1 parent f1462a1 commit 74bec09
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 9 deletions.
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,16 @@ class AnimalStore extends Store {
class Animal extends Model {
@observable id = null;
@observable name = '';

@observable breed = this.relation(Bread);
@observable relatives = this.relation(AnimalStore)

relations() {
return {
breed: Breed, // Define a breed relation to Breed.
relatives: AnimalStore, // Define a relatives relation to AnimalStore.
};
}
}
```

Note that it is really import for relations to be observable, otherwise the parsing of the model data will fail.


You can now instantiate the animal with it's breed & relatives relation recursively:

```js
Expand Down Expand Up @@ -314,6 +314,37 @@ 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.
```

### Alternative legacy relation definition

Alternatively, relations can be defined by overriding the relations(). This is used in old models. Note that this way of defining models has two disadvantages:

- It doesn't allow inheriting relations
- It doesn't allow for easy type hinting in typescript

```js
class Breed extends Model {
@observable id = null;
@observable name = '';
}

class AnimalStore extends Store {
Model = Animal;
}

class Animal extends Model {
@observable id = null;
@observable name = '';

relations() {
return {
breed: Breed, // Define a breed relation to Breed.
relatives: AnimalStore, // Define a relatives relation to AnimalStore.
};
}
}
```


### 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.
Expand Down
41 changes: 40 additions & 1 deletion dist/mobx-spine.cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,23 @@ var Store = (_class = (_temp = _class2 = function () {
}
}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype)), _class);

var Relation = function () {
function Relation(toModel) {
classCallCheck(this, Relation);
this.__toModel = null;

this.__toModel = toModel;
}

createClass(Relation, [{
key: "model",
get: function get() {
return this.__toModel;
}
}]);
return Relation;
}();

var _class$1, _descriptor$1, _descriptor2$1, _descriptor3$1, _descriptor4$1, _descriptor5$1, _descriptor6, _descriptor7, _class2$1, _temp$1;

function _initDefineProp$1(target, property, descriptor, context) {
Expand Down Expand Up @@ -995,13 +1012,20 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () {

_initDefineProp$1(this, '__fileExists', _descriptor7, this);

this.__relations = {};

this.__store = options.store;
this.__repository = options.repository;
this.abortController = new AbortController();

// Find all attributes. Not all observables are an attribute.
lodash.forIn(this, function (value, key) {
if (!key.startsWith('__') && mobx.isObservableProp(_this2, key)) {

// Register relations
if (value instanceof Relation) {
_this2.__relations[key] = value.model;
_this2[key] = undefined;
} else if (!key.startsWith('__') && mobx.isObservableProp(_this2, key)) {
invariant(!FORBIDDEN_ATTRS.includes(key), 'Forbidden attribute key used: `' + key + '`');
_this2.__attributes.push(key);
var newValue = value;
Expand Down Expand Up @@ -1801,6 +1825,21 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () {
_this18[currentRel].clear();
});
}

/**************
* New way of doing relations
*************/

}, {
key: 'relation',
value: function relation(modelOrSTore) {
return new Relation(modelOrSTore);
}
}, {
key: 'relations',
value: function relations() {
return this.__relations;
}
}, {
key: 'hasUserChanges',
get: function get() {
Expand Down
41 changes: 40 additions & 1 deletion dist/mobx-spine.es.js
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,23 @@ var Store = (_class = (_temp = _class2 = function () {
}
}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype)), _class);

var Relation = function () {
function Relation(toModel) {
classCallCheck(this, Relation);
this.__toModel = null;

this.__toModel = toModel;
}

createClass(Relation, [{
key: "model",
get: function get() {
return this.__toModel;
}
}]);
return Relation;
}();

var _class$1, _descriptor$1, _descriptor2$1, _descriptor3$1, _descriptor4$1, _descriptor5$1, _descriptor6, _descriptor7, _class2$1, _temp$1;

function _initDefineProp$1(target, property, descriptor, context) {
Expand Down Expand Up @@ -989,13 +1006,20 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () {

_initDefineProp$1(this, '__fileExists', _descriptor7, this);

this.__relations = {};

this.__store = options.store;
this.__repository = options.repository;
this.abortController = new AbortController();

// Find all attributes. Not all observables are an attribute.
forIn(this, function (value, key) {
if (!key.startsWith('__') && isObservableProp(_this2, key)) {

// Register relations
if (value instanceof Relation) {
_this2.__relations[key] = value.model;
_this2[key] = undefined;
} else if (!key.startsWith('__') && isObservableProp(_this2, key)) {
invariant(!FORBIDDEN_ATTRS.includes(key), 'Forbidden attribute key used: `' + key + '`');
_this2.__attributes.push(key);
var newValue = value;
Expand Down Expand Up @@ -1795,6 +1819,21 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () {
_this18[currentRel].clear();
});
}

/**************
* New way of doing relations
*************/

}, {
key: 'relation',
value: function relation(modelOrSTore) {
return new Relation(modelOrSTore);
}
}, {
key: 'relations',
value: function relations() {
return this.__relations;
}
}, {
key: 'hasUserChanges',
get: function get() {
Expand Down
21 changes: 20 additions & 1 deletion src/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import Store from './Store';
import { invariant, snakeToCamel, camelToSnake, relationsToNestedKeys, forNestedRelations } from './utils';
import Axios from 'axios';
import {Relation} from "./Relations";

function concatInDict(dict, key, value) {
dict[key] = dict[key] ? dict[key].concat(value) : value;
Expand Down Expand Up @@ -172,7 +173,12 @@ export default class Model {

// Find all attributes. Not all observables are an attribute.
forIn(this, (value, key) => {
if (!key.startsWith('__') && isObservableProp(this, key)) {

// Register relations
if (value instanceof Relation) {
this.__relations[key] = value.model;
this[key] = undefined
} else if (!key.startsWith('__') && isObservableProp(this, key)) {
invariant(
!FORBIDDEN_ATTRS.includes(key),
`Forbidden attribute key used: \`${key}\``
Expand Down Expand Up @@ -966,4 +972,17 @@ export default class Model {
this[currentRel].clear();
});
}

/**************
* New way of doing relations
*************/
__relations = {}

relation(modelOrSTore) {
return new Relation(modelOrSTore)
}

relations() {
return this.__relations;
}
}
11 changes: 11 additions & 0 deletions src/Relations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class Relation {
__toModel = null;

constructor(toModel) {
this.__toModel = toModel
}

get model() {
return this.__toModel
}
}
69 changes: 69 additions & 0 deletions src/__tests__/NewRelation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Kind, Person, PersonStore} from "./fixtures/Animal";
import {observable} from "mobx";
import Model from "../Model";
import {BinderApi} from "../index";

class Animal extends Model {
urlRoot = '/api/animal/';
api = new BinderApi();
static backendResourceName = 'animal';
@observable id = null;
@observable name = '';

@observable kind = this.relation(Kind);

@observable owner = this.relation(Person);

@observable pastOwner = this.relation(PersonStore)

@observable father = this.relation(Animal)
}

describe('Model with new way of defining relations', () => {
test('Initialize model with valid data', () => {
const animal = new Animal({})

Check warning on line 24 in src/__tests__/NewRelation.js

View workflow job for this annotation

GitHub Actions / check (14)

'animal' is assigned a value but never used
});

test('Relation should not be initialized by default', () => {
const animal = new Animal();

expect(animal.kind).toBeUndefined();
});

test('Initialize one-level relation', () => {
const animal = new Animal(null, {
relations: ['kind'],
});

expect(animal.kind).toBeInstanceOf(Kind);
});
test('Initialize multiple relations', () => {
const animal = new Animal(null, {
relations: ['kind', 'owner'],
});

expect(animal.kind).toBeInstanceOf(Kind);
expect(animal.owner).toBeInstanceOf(Person);
});

test('Non existent relation should throw an error', () => {
expect(() => {
return new Animal(null, {
relations: ['ponyfoo'],
});
}).toThrow('Specified relation "ponyfoo" does not exist on model.');
});

test('Can have a relation to itself', () => {
const animal = new Animal(null, {
relations: ['father'],
});

expect(animal.father).toBeInstanceOf(Animal);
})


})



0 comments on commit 74bec09

Please sign in to comment.