Skip to content

Commit

Permalink
feat(model): add support to nested keys for relations (#127)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
JoaoPedroAS51 authored Oct 22, 2020
1 parent 1bc6248 commit 095c1c3
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 15 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ await this.posts.comments().attach(payload)
await this.posts.comments().sync(payload)
```

You can also apply a model instance to a nested object by setting the key and the model in `relations` method.
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.

If the backend responds with:

Expand All @@ -529,11 +529,16 @@ If the backend responds with:
user: {
firstName: 'John',
lastName: 'Doe'
},
relationships: {
tag: {
name: 'awesome'
}
}
}
```

We just need to set `user` to User model:
We just need to set `user` to User model and `relationships.tag`to Tag model:


**/models/Post.js**
Expand All @@ -542,7 +547,9 @@ class Post extends Model {
relations () {
return {
// Apply User model to `user` object
user: User
user: User,
// Apply Tag model to `relationships.tag` object
'relationships.tag': Tag
}
}
}
Expand Down
17 changes: 10 additions & 7 deletions src/Model.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Builder from './Builder';
import StaticModel from './StaticModel';
import { getProp, setProp } from './utils'

export default class Model extends StaticModel {

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

for(const relation of Object.keys(relations)) {
if (!model[relation]) {
const _relation = getProp(model, relation)

if (!_relation) {
return;
}

if (Array.isArray(model[relation].data) || Array.isArray(model[relation])) {
const collection = this._applyInstanceCollection(model[relation], relations[relation])
if (Array.isArray(_relation.data) || Array.isArray(_relation)) {
const collection = this._applyInstanceCollection(_relation, relations[relation])

if (model[relation].data !== undefined) {
model[relation].data = collection
if (_relation.data !== undefined) {
_relation.data = collection
} else {
model[relation] = collection
setProp(model, relation, collection)
}
} else {
model[relation] = this._applyInstance(model[relation], relations[relation])
setProp(model, relation, this._applyInstance(_relation, relations[relation]))
}
}
}
Expand Down
44 changes: 44 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Get property defined by dot notation in string.
* Based on https://github.com/dy/dotprop (MIT)
*
* @param {Object} holder Target object where to look property up
* @param {string} propName Dot notation, like 'this.a.b.c'
* @return {*} A property value
*/
export function getProp (holder, propName) {
if (!propName || !holder) {
return holder
}

if (propName in holder) {
return holder[propName]
}

const propParts = Array.isArray(propName) ? propName : (propName + '').split('.')

let result = holder
while (propParts.length && result) {
result = result[propParts.shift()]
}

return result
}

/**
* Set property defined by dot notation in string.
* Based on https://github.com/lukeed/dset (MIT)
*
* @param {Object} holder Target object where to look property up
* @param {string} propName Dot notation, like 'this.a.b.c'
* @param {*} value The value to be set
*/
export function setProp (holder, propName, value) {
const propParts = Array.isArray(propName) ? propName : (propName + '').split('.')
let i = 0, l = propParts.length, t = holder, x

for (; i < l; ++i) {
x = t[propParts[i]]
t = t[propParts[i]] = (i === l - 1 ? value : (x != null ? x : (!!~propParts[i + 1].indexOf('.') || !(+propParts[i + 1] > -1)) ? {} : []))
}
}
10 changes: 10 additions & 0 deletions tests/dummy/data/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,15 @@ export const Post =
firstname: 'John',
lastname: 'Doe',
age: 25
},
relationships: {
tags: [
{
name: 'super'
},
{
name: 'awesome'
}
]
}
}
12 changes: 12 additions & 0 deletions tests/dummy/data/postEmbed.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ export const Post = {
firstname: 'John',
lastname: 'Doe',
age: 25
},
relationships: {
tags: {
data: [
{
name: 'super'
},
{
name: 'awesome'
}
]
}
}
}
}
20 changes: 20 additions & 0 deletions tests/dummy/data/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ export const Posts = [
firstname: 'John',
lastname: 'Doe',
age: 25
},
relationships: {
tags: [
{
name: 'super'
},
{
name: 'awesome'
}
]
}
},
{
Expand All @@ -17,6 +27,16 @@ export const Posts = [
firstname: 'Mary',
lastname: 'Doe',
age: 25
},
relationships: {
tags: [
{
name: 'super'
},
{
name: 'awesome'
}
]
}
}
]
22 changes: 22 additions & 0 deletions tests/dummy/data/postsEmbed.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ export const Posts = {
firstname: 'John',
lastname: 'Doe',
age: 25
},
relationships: {
tags: [
{
name: 'super'
},
{
name: 'awesome'
}
]
}
},
{
Expand All @@ -18,6 +28,18 @@ export const Posts = {
firstname: 'Mary',
lastname: 'Doe',
age: 25
},
relationships: {
tags: {
data: [
{
name: 'super'
},
{
name: 'awesome'
}
]
}
}
}
]
Expand Down
4 changes: 3 additions & 1 deletion tests/dummy/models/Post.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import BaseModel from './BaseModel'
import Comment from './Comment'
import User from './User'
import Tag from './Tag'

export default class Post extends BaseModel {
comments () {
Expand All @@ -9,7 +10,8 @@ export default class Post extends BaseModel {

relations () {
return {
user: User
user: User,
'relationships.tags': Tag
}
}
}
5 changes: 5 additions & 0 deletions tests/dummy/models/Tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import BaseModel from './BaseModel'

export default class Tag extends BaseModel {
//
}
39 changes: 35 additions & 4 deletions tests/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import User from './dummy/models/User'
import Comment from './dummy/models/Comment'
import { Model } from '../src'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter';
import MockAdapter from 'axios-mock-adapter'
import { Posts as postsResponse } from './dummy/data/posts'
import { Posts as postsEmbedResponse } from './dummy/data/postsEmbed'
import { Post as postResponse } from './dummy/data/post'
import { Post as postEmbedResponse } from './dummy/data/postEmbed'
import { Comments as commentsResponse } from './dummy/data/comments'
import { Comments as commentsEmbedResponse } from './dummy/data/commentsEmbed'
import Tag from './dummy/models/Tag'

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

Expand Down Expand Up @@ -50,6 +51,9 @@ describe('Model methods', () => {
expect(post).toEqual(postsResponse[0])
expect(post).toBeInstanceOf(Post)
expect(post.user).toBeInstanceOf(User)
post.relationships.tags.forEach(tag => {
expect(tag).toBeInstanceOf(Tag)
})
})

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

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

test('$find() handles request with "data" wrapper', async () => {
Expand All @@ -87,6 +97,9 @@ describe('Model methods', () => {
expect(post).toEqual(postEmbedResponse.data)
expect(post).toBeInstanceOf(Post)
expect(post.user).toBeInstanceOf(User)
post.relationships.tags.data.forEach(tag => {
expect(tag).toBeInstanceOf(Tag)
})
})

test('$find() handles request without "data" wrapper', async () => {
Expand All @@ -97,7 +110,9 @@ describe('Model methods', () => {
expect(post).toEqual(postResponse)
expect(post).toBeInstanceOf(Post)
expect(post.user).toBeInstanceOf(User)

post.relationships.tags.forEach(tag => {
expect(tag).toBeInstanceOf(Tag)
})
})

test('get() method returns a array of objects as instance of suchModel', async () => {
Expand All @@ -108,7 +123,10 @@ describe('Model methods', () => {
posts.forEach(post => {
expect(post).toBeInstanceOf(Post)
expect(post.user).toBeInstanceOf(User)
});
post.relationships.tags.forEach(tag => {
expect(tag).toBeInstanceOf(Tag)
})
})
})

test('get() hits right resource (nested object)', async () => {
Expand Down Expand Up @@ -225,6 +243,16 @@ describe('Model methods', () => {
firstname: 'John',
lastname: 'Doe',
age: 25
},
relationships: {
tags: [
{
name: 'super'
},
{
name: 'awesome'
}
]
}
}

Expand All @@ -242,6 +270,9 @@ describe('Model methods', () => {
expect(post).toEqual(_postResponse)
expect(post).toBeInstanceOf(Post)
expect(post.user).toBeInstanceOf(User)
post.relationships.tags.forEach(tag => {
expect(tag).toBeInstanceOf(Tag)
})
})

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

posts.forEach(post => {
expect(post).toBeInstanceOf(Post)
});
})
})

test('attach() method hits right endpoint with a POST request', async () => {
Expand Down
33 changes: 33 additions & 0 deletions tests/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getProp, setProp } from '../src/utils'

describe('Utilities', () => {
test('Get property defined by dot notation in string.', () => {
const holder = {
a: {
b: {
c: 1
}
}
}

const result = getProp(holder, 'a.b.c')

expect(result).toBe(1)
})

test('Set property defined by dot notation in string.', () => {
const holder = {
a: {
b: {
c: 1
}
}
}

setProp(holder, 'a.b.c', 2)

const result = getProp(holder, 'a.b.c')

expect(result).toBe(2)
})
})

0 comments on commit 095c1c3

Please sign in to comment.