From 095c1c3c025faa4e44c6ab626588179fb9be5140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?= Date: Wed, 21 Oct 2020 22:20:28 -0300 Subject: [PATCH] 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 --- README.md | 13 +++++++--- src/Model.js | 17 +++++++------ src/utils.js | 44 ++++++++++++++++++++++++++++++++++ tests/dummy/data/post.js | 10 ++++++++ tests/dummy/data/postEmbed.js | 12 ++++++++++ tests/dummy/data/posts.js | 20 ++++++++++++++++ tests/dummy/data/postsEmbed.js | 22 +++++++++++++++++ tests/dummy/models/Post.js | 4 +++- tests/dummy/models/Tag.js | 5 ++++ tests/model.test.js | 39 ++++++++++++++++++++++++++---- tests/utils.test.js | 33 +++++++++++++++++++++++++ 11 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 src/utils.js create mode 100644 tests/dummy/models/Tag.js create mode 100644 tests/utils.test.js diff --git a/README.md b/README.md index 2f3bce3..2e6c4a2 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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** @@ -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 } } } diff --git a/src/Model.js b/src/Model.js index 8f1bbe7..b4da026 100644 --- a/src/Model.js +++ b/src/Model.js @@ -1,5 +1,6 @@ import Builder from './Builder'; import StaticModel from './StaticModel'; +import { getProp, setProp } from './utils' export default class Model extends StaticModel { @@ -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])) } } } diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..46b598a --- /dev/null +++ b/src/utils.js @@ -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)) ? {} : [])) + } +} diff --git a/tests/dummy/data/post.js b/tests/dummy/data/post.js index 6bc4404..db180c4 100644 --- a/tests/dummy/data/post.js +++ b/tests/dummy/data/post.js @@ -7,5 +7,15 @@ export const Post = firstname: 'John', lastname: 'Doe', age: 25 + }, + relationships: { + tags: [ + { + name: 'super' + }, + { + name: 'awesome' + } + ] } } diff --git a/tests/dummy/data/postEmbed.js b/tests/dummy/data/postEmbed.js index 96e89dd..1596d89 100644 --- a/tests/dummy/data/postEmbed.js +++ b/tests/dummy/data/postEmbed.js @@ -7,6 +7,18 @@ export const Post = { firstname: 'John', lastname: 'Doe', age: 25 + }, + relationships: { + tags: { + data: [ + { + name: 'super' + }, + { + name: 'awesome' + } + ] + } } } } diff --git a/tests/dummy/data/posts.js b/tests/dummy/data/posts.js index eaadead..807ab50 100644 --- a/tests/dummy/data/posts.js +++ b/tests/dummy/data/posts.js @@ -7,6 +7,16 @@ export const Posts = [ firstname: 'John', lastname: 'Doe', age: 25 + }, + relationships: { + tags: [ + { + name: 'super' + }, + { + name: 'awesome' + } + ] } }, { @@ -17,6 +27,16 @@ export const Posts = [ firstname: 'Mary', lastname: 'Doe', age: 25 + }, + relationships: { + tags: [ + { + name: 'super' + }, + { + name: 'awesome' + } + ] } } ] diff --git a/tests/dummy/data/postsEmbed.js b/tests/dummy/data/postsEmbed.js index 82a8cbe..581327c 100644 --- a/tests/dummy/data/postsEmbed.js +++ b/tests/dummy/data/postsEmbed.js @@ -8,6 +8,16 @@ export const Posts = { firstname: 'John', lastname: 'Doe', age: 25 + }, + relationships: { + tags: [ + { + name: 'super' + }, + { + name: 'awesome' + } + ] } }, { @@ -18,6 +28,18 @@ export const Posts = { firstname: 'Mary', lastname: 'Doe', age: 25 + }, + relationships: { + tags: { + data: [ + { + name: 'super' + }, + { + name: 'awesome' + } + ] + } } } ] diff --git a/tests/dummy/models/Post.js b/tests/dummy/models/Post.js index ade629f..228aec2 100644 --- a/tests/dummy/models/Post.js +++ b/tests/dummy/models/Post.js @@ -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 () { @@ -9,7 +10,8 @@ export default class Post extends BaseModel { relations () { return { - user: User + user: User, + 'relationships.tags': Tag } } } diff --git a/tests/dummy/models/Tag.js b/tests/dummy/models/Tag.js new file mode 100644 index 0000000..28b6e7a --- /dev/null +++ b/tests/dummy/models/Tag.js @@ -0,0 +1,5 @@ +import BaseModel from './BaseModel' + +export default class Tag extends BaseModel { + // +} diff --git a/tests/model.test.js b/tests/model.test.js index 59fccb3..2fb467d 100644 --- a/tests/model.test.js +++ b/tests/model.test.js @@ -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', () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -225,6 +243,16 @@ describe('Model methods', () => { firstname: 'John', lastname: 'Doe', age: 25 + }, + relationships: { + tags: [ + { + name: 'super' + }, + { + name: 'awesome' + } + ] } } @@ -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 () => { @@ -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 () => { diff --git a/tests/utils.test.js b/tests/utils.test.js new file mode 100644 index 0000000..6dc0703 --- /dev/null +++ b/tests/utils.test.js @@ -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) + }) +})