Skip to content

Commit 095c1c3

Browse files
feat(model): add support to nested keys for relations (#127)
* feat(utils): add utilities `getProp` and `setProp` * feat(model): add support to nested keys for relations * test(model): test support of nested keys for relations * test(utils): test utilities * chore: update readme
1 parent 1bc6248 commit 095c1c3

11 files changed

+204
-15
lines changed

README.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ await this.posts.comments().attach(payload)
517517
await this.posts.comments().sync(payload)
518518
```
519519

520-
You can also apply a model instance to a nested object by setting the key and the model in `relations` method.
520+
You can also apply a model instance to a nested object by setting the key and the model in `relations` method. It supports nested keys.
521521

522522
If the backend responds with:
523523

@@ -529,11 +529,16 @@ If the backend responds with:
529529
user: {
530530
firstName: 'John',
531531
lastName: 'Doe'
532+
},
533+
relationships: {
534+
tag: {
535+
name: 'awesome'
536+
}
532537
}
533538
}
534539
```
535540

536-
We just need to set `user` to User model:
541+
We just need to set `user` to User model and `relationships.tag`to Tag model:
537542

538543

539544
**/models/Post.js**
@@ -542,7 +547,9 @@ class Post extends Model {
542547
relations () {
543548
return {
544549
// Apply User model to `user` object
545-
user: User
550+
user: User,
551+
// Apply Tag model to `relationships.tag` object
552+
'relationships.tag': Tag
546553
}
547554
}
548555
}

src/Model.js

+10-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Builder from './Builder';
22
import StaticModel from './StaticModel';
3+
import { getProp, setProp } from './utils'
34

45
export default class Model extends StaticModel {
56

@@ -255,20 +256,22 @@ export default class Model extends StaticModel {
255256
const relations = model.relations()
256257

257258
for(const relation of Object.keys(relations)) {
258-
if (!model[relation]) {
259+
const _relation = getProp(model, relation)
260+
261+
if (!_relation) {
259262
return;
260263
}
261264

262-
if (Array.isArray(model[relation].data) || Array.isArray(model[relation])) {
263-
const collection = this._applyInstanceCollection(model[relation], relations[relation])
265+
if (Array.isArray(_relation.data) || Array.isArray(_relation)) {
266+
const collection = this._applyInstanceCollection(_relation, relations[relation])
264267

265-
if (model[relation].data !== undefined) {
266-
model[relation].data = collection
268+
if (_relation.data !== undefined) {
269+
_relation.data = collection
267270
} else {
268-
model[relation] = collection
271+
setProp(model, relation, collection)
269272
}
270273
} else {
271-
model[relation] = this._applyInstance(model[relation], relations[relation])
274+
setProp(model, relation, this._applyInstance(_relation, relations[relation]))
272275
}
273276
}
274277
}

src/utils.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Get property defined by dot notation in string.
3+
* Based on https://github.com/dy/dotprop (MIT)
4+
*
5+
* @param {Object} holder Target object where to look property up
6+
* @param {string} propName Dot notation, like 'this.a.b.c'
7+
* @return {*} A property value
8+
*/
9+
export function getProp (holder, propName) {
10+
if (!propName || !holder) {
11+
return holder
12+
}
13+
14+
if (propName in holder) {
15+
return holder[propName]
16+
}
17+
18+
const propParts = Array.isArray(propName) ? propName : (propName + '').split('.')
19+
20+
let result = holder
21+
while (propParts.length && result) {
22+
result = result[propParts.shift()]
23+
}
24+
25+
return result
26+
}
27+
28+
/**
29+
* Set property defined by dot notation in string.
30+
* Based on https://github.com/lukeed/dset (MIT)
31+
*
32+
* @param {Object} holder Target object where to look property up
33+
* @param {string} propName Dot notation, like 'this.a.b.c'
34+
* @param {*} value The value to be set
35+
*/
36+
export function setProp (holder, propName, value) {
37+
const propParts = Array.isArray(propName) ? propName : (propName + '').split('.')
38+
let i = 0, l = propParts.length, t = holder, x
39+
40+
for (; i < l; ++i) {
41+
x = t[propParts[i]]
42+
t = t[propParts[i]] = (i === l - 1 ? value : (x != null ? x : (!!~propParts[i + 1].indexOf('.') || !(+propParts[i + 1] > -1)) ? {} : []))
43+
}
44+
}

tests/dummy/data/post.js

+10
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,15 @@ export const Post =
77
firstname: 'John',
88
lastname: 'Doe',
99
age: 25
10+
},
11+
relationships: {
12+
tags: [
13+
{
14+
name: 'super'
15+
},
16+
{
17+
name: 'awesome'
18+
}
19+
]
1020
}
1121
}

tests/dummy/data/postEmbed.js

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ export const Post = {
77
firstname: 'John',
88
lastname: 'Doe',
99
age: 25
10+
},
11+
relationships: {
12+
tags: {
13+
data: [
14+
{
15+
name: 'super'
16+
},
17+
{
18+
name: 'awesome'
19+
}
20+
]
21+
}
1022
}
1123
}
1224
}

tests/dummy/data/posts.js

+20
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ export const Posts = [
77
firstname: 'John',
88
lastname: 'Doe',
99
age: 25
10+
},
11+
relationships: {
12+
tags: [
13+
{
14+
name: 'super'
15+
},
16+
{
17+
name: 'awesome'
18+
}
19+
]
1020
}
1121
},
1222
{
@@ -17,6 +27,16 @@ export const Posts = [
1727
firstname: 'Mary',
1828
lastname: 'Doe',
1929
age: 25
30+
},
31+
relationships: {
32+
tags: [
33+
{
34+
name: 'super'
35+
},
36+
{
37+
name: 'awesome'
38+
}
39+
]
2040
}
2141
}
2242
]

tests/dummy/data/postsEmbed.js

+22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ export const Posts = {
88
firstname: 'John',
99
lastname: 'Doe',
1010
age: 25
11+
},
12+
relationships: {
13+
tags: [
14+
{
15+
name: 'super'
16+
},
17+
{
18+
name: 'awesome'
19+
}
20+
]
1121
}
1222
},
1323
{
@@ -18,6 +28,18 @@ export const Posts = {
1828
firstname: 'Mary',
1929
lastname: 'Doe',
2030
age: 25
31+
},
32+
relationships: {
33+
tags: {
34+
data: [
35+
{
36+
name: 'super'
37+
},
38+
{
39+
name: 'awesome'
40+
}
41+
]
42+
}
2143
}
2244
}
2345
]

tests/dummy/models/Post.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import BaseModel from './BaseModel'
22
import Comment from './Comment'
33
import User from './User'
4+
import Tag from './Tag'
45

56
export default class Post extends BaseModel {
67
comments () {
@@ -9,7 +10,8 @@ export default class Post extends BaseModel {
910

1011
relations () {
1112
return {
12-
user: User
13+
user: User,
14+
'relationships.tags': Tag
1315
}
1416
}
1517
}

tests/dummy/models/Tag.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import BaseModel from './BaseModel'
2+
3+
export default class Tag extends BaseModel {
4+
//
5+
}

tests/model.test.js

+35-4
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import User from './dummy/models/User'
33
import Comment from './dummy/models/Comment'
44
import { Model } from '../src'
55
import axios from 'axios'
6-
import MockAdapter from 'axios-mock-adapter';
6+
import MockAdapter from 'axios-mock-adapter'
77
import { Posts as postsResponse } from './dummy/data/posts'
88
import { Posts as postsEmbedResponse } from './dummy/data/postsEmbed'
99
import { Post as postResponse } from './dummy/data/post'
1010
import { Post as postEmbedResponse } from './dummy/data/postEmbed'
1111
import { Comments as commentsResponse } from './dummy/data/comments'
1212
import { Comments as commentsEmbedResponse } from './dummy/data/commentsEmbed'
13+
import Tag from './dummy/models/Tag'
1314

1415
describe('Model methods', () => {
1516

@@ -50,6 +51,9 @@ describe('Model methods', () => {
5051
expect(post).toEqual(postsResponse[0])
5152
expect(post).toBeInstanceOf(Post)
5253
expect(post.user).toBeInstanceOf(User)
54+
post.relationships.tags.forEach(tag => {
55+
expect(tag).toBeInstanceOf(Tag)
56+
})
5357
})
5458

5559
test('$first() returns first object in array as instance of such Model', async () => {
@@ -61,6 +65,9 @@ describe('Model methods', () => {
6165
expect(post).toEqual(postsEmbedResponse.data[0])
6266
expect(post).toBeInstanceOf(Post)
6367
expect(post.user).toBeInstanceOf(User)
68+
post.relationships.tags.forEach(tag => {
69+
expect(tag).toBeInstanceOf(Tag)
70+
})
6471
})
6572

6673
test('first() method returns a empty object when no items have found', async () => {
@@ -77,6 +84,9 @@ describe('Model methods', () => {
7784
expect(post).toEqual(postResponse)
7885
expect(post).toBeInstanceOf(Post)
7986
expect(post.user).toBeInstanceOf(User)
87+
post.relationships.tags.forEach(tag => {
88+
expect(tag).toBeInstanceOf(Tag)
89+
})
8090
})
8191

8292
test('$find() handles request with "data" wrapper', async () => {
@@ -87,6 +97,9 @@ describe('Model methods', () => {
8797
expect(post).toEqual(postEmbedResponse.data)
8898
expect(post).toBeInstanceOf(Post)
8999
expect(post.user).toBeInstanceOf(User)
100+
post.relationships.tags.data.forEach(tag => {
101+
expect(tag).toBeInstanceOf(Tag)
102+
})
90103
})
91104

92105
test('$find() handles request without "data" wrapper', async () => {
@@ -97,7 +110,9 @@ describe('Model methods', () => {
97110
expect(post).toEqual(postResponse)
98111
expect(post).toBeInstanceOf(Post)
99112
expect(post.user).toBeInstanceOf(User)
100-
113+
post.relationships.tags.forEach(tag => {
114+
expect(tag).toBeInstanceOf(Tag)
115+
})
101116
})
102117

103118
test('get() method returns a array of objects as instance of suchModel', async () => {
@@ -108,7 +123,10 @@ describe('Model methods', () => {
108123
posts.forEach(post => {
109124
expect(post).toBeInstanceOf(Post)
110125
expect(post.user).toBeInstanceOf(User)
111-
});
126+
post.relationships.tags.forEach(tag => {
127+
expect(tag).toBeInstanceOf(Tag)
128+
})
129+
})
112130
})
113131

114132
test('get() hits right resource (nested object)', async () => {
@@ -225,6 +243,16 @@ describe('Model methods', () => {
225243
firstname: 'John',
226244
lastname: 'Doe',
227245
age: 25
246+
},
247+
relationships: {
248+
tags: [
249+
{
250+
name: 'super'
251+
},
252+
{
253+
name: 'awesome'
254+
}
255+
]
228256
}
229257
}
230258

@@ -242,6 +270,9 @@ describe('Model methods', () => {
242270
expect(post).toEqual(_postResponse)
243271
expect(post).toBeInstanceOf(Post)
244272
expect(post.user).toBeInstanceOf(User)
273+
post.relationships.tags.forEach(tag => {
274+
expect(tag).toBeInstanceOf(Tag)
275+
})
245276
})
246277

247278
test('save() method makes a PUT request when ID of object exists', async () => {
@@ -490,7 +521,7 @@ describe('Model methods', () => {
490521

491522
posts.forEach(post => {
492523
expect(post).toBeInstanceOf(Post)
493-
});
524+
})
494525
})
495526

496527
test('attach() method hits right endpoint with a POST request', async () => {

tests/utils.test.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { getProp, setProp } from '../src/utils'
2+
3+
describe('Utilities', () => {
4+
test('Get property defined by dot notation in string.', () => {
5+
const holder = {
6+
a: {
7+
b: {
8+
c: 1
9+
}
10+
}
11+
}
12+
13+
const result = getProp(holder, 'a.b.c')
14+
15+
expect(result).toBe(1)
16+
})
17+
18+
test('Set property defined by dot notation in string.', () => {
19+
const holder = {
20+
a: {
21+
b: {
22+
c: 1
23+
}
24+
}
25+
}
26+
27+
setProp(holder, 'a.b.c', 2)
28+
29+
const result = getProp(holder, 'a.b.c')
30+
31+
expect(result).toBe(2)
32+
})
33+
})

0 commit comments

Comments
 (0)