Skip to content

Add 'fetch' based methods for first() and find()... #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/build/
/coverage/
yarn-error.log
docker-compose.yml
docker-compose.yml
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -629,9 +629,10 @@ This package automatically handles the response from backend and convert it into

## Single object

If your backend responds with a single object as a **ROOT ELEMENT** like this:
If your backend responds with the following response...

```js
// Note: the response data is the root element with no 'data' wrapper
{
id: 1,
firstname: 'John',
Expand All @@ -640,21 +641,49 @@ If your backend responds with a single object as a **ROOT ELEMENT** like this:
}
```

So, `find()` and `first()` methods automatically will convert the backend response into an instace of `User` model.
Then using `find()` and `first()` will work as expected and will hydrate the response into the `User` Model.

```js
let user = await User.find(1)

//or
// or

let user = await User.first()

// will work - an instance of User was created from response

user.makeBirthday()
```

However, if the backend sends a response like this...

```js
// Note: the response is wrapped with 'data' attribute...
data: {
{
id: 1,
firstname: 'John',
lastname: 'Doe',
age: 25
}
}
```

...then the above would fail. If your backend wraps single objects with a data attribute too then you should use the fetch method of find (which is `$find()`) instead to automatically hydrate the Model with the response data:

```js
let user = await User.$find(1)

// or

let user = await User.$first()

// will work, because an instance of User was created from response

user.makeBirthday()
```

This **WILL NOT** be converted into `User` model, because the main data is not the root element.
This **WILL NOT** be converted into `User` model, because the main data is not the root element or it is not wrapped by `data` attribute.

```js
user: {
Expand Down
30 changes: 23 additions & 7 deletions src/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import StaticModel from './StaticModel';

export default class Model extends StaticModel {

constructor(...atributtes) {
constructor(...attributes) {
super()

if (atributtes.length === 0) {
if (attributes.length === 0) {
this._builder = new Builder(this)
} else {
Object.assign(this, ...atributtes)
Object.assign(this, ...attributes)
}

if (this.baseURL === undefined) {
Expand Down Expand Up @@ -57,28 +57,28 @@ export default class Model extends StaticModel {
// We handle this implementation detail here to simplify the readme.
let slash = '';
let resource = '';

args.forEach(value => {
switch(true) {
case (typeof value === 'string'):
resource += slash + value.replace(/^\/+/, '');
break;
case (value instanceof Model):
resource += slash + value.resource();

if(value.isValidId(value.getPrimaryKey())) {
resource += '/' + value.getPrimaryKey();
}
break;
default:
throw new Error('Arguments to custom() must be strings or instances of Model.')
}

if( !slash.length ) {
slash = '/';
}
});

this._customResource = resource

return this
Expand Down Expand Up @@ -240,6 +240,12 @@ export default class Model extends StaticModel {
})
}

$first() {
return this
.first()
.then(response => response.data || response)
}

find(identifier) {
if (identifier === undefined) {
throw new Error('You must specify the param on find() method.')
Expand All @@ -253,6 +259,16 @@ export default class Model extends StaticModel {
}).then(response => new this.constructor(response.data))
}

$find(identifier) {
if (identifier === undefined) {
throw new Error('You must specify the param on $find() method.')
}

return this
.find(identifier)
.then(response => response.data || response)
}

get() {
let base = this._fromResource || `${this.baseURL()}/${this.resource()}`
base = this._customResource ? `${this.baseURL()}/${this._customResource}` : base
Expand Down
18 changes: 15 additions & 3 deletions src/StaticModel.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* Provide static calls for all methods.
*
*
* Instead this: let users = new User().with('country').get()
* We can do this: let users = User.with('conutry').get()
*
*
*/

export default class StaticModel {
Expand Down Expand Up @@ -87,12 +87,24 @@ export default class StaticModel {
return self.first()
}

static $first() {
let self = this.instance()

return self.$first()
}

static find(id) {
let self = this.instance()

return self.find(id)
}

static $find(id) {
let self = this.instance()

return self.$find(id)
}

static get() {
let self = this.instance()

Expand All @@ -104,4 +116,4 @@ export default class StaticModel {

return self.$get()
}
}
}
9 changes: 9 additions & 0 deletions tests/dummy/data/postEmbed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const Post = {
data: {
id: 1,
someId: 'af621-4aa41',
firstname: 'John',
lastname: 'Doe',
age: 25
}
}
47 changes: 41 additions & 6 deletions tests/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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'

describe('Model methods', () => {
Expand All @@ -30,6 +31,14 @@ describe('Model methods', () => {
expect(errorModel).toThrow('You must specify the param on find() method.')
})

test('it throws a error when $find() has no parameters', () => {
errorModel = () => {
const post = Post.$find()
}

expect(errorModel).toThrow('You must specify the param on $find() method.')
})

test('first() returns first object in array as instance of such Model', async () => {

axiosMock.onGet('http://localhost/posts').reply(200, {
Expand All @@ -41,6 +50,16 @@ describe('Model methods', () => {
expect(post).toBeInstanceOf(Post)
})

test('$first() returns first object in array as instance of such Model', async () => {

axiosMock.onGet('http://localhost/posts').reply(200, postsEmbedResponse)

const post = await Post.$first()

expect(post).toEqual(postsEmbedResponse.data[0])
expect(post).toBeInstanceOf(Post)
})

test('first() method returns a empty object when no items have found', async () => {
axiosMock.onGet('http://localhost/posts').reply(200, [])

Expand All @@ -56,7 +75,23 @@ describe('Model methods', () => {
expect(post).toBeInstanceOf(Post)
})

test('get() method returns a array of objects as instace of suchModel', async () => {
test('$find() handles request with "data" wrapper', async () => {
axiosMock.onGet('http://localhost/posts/1').reply(200, postEmbedResponse)

const post = await Post.$find(1)

expect(post).toEqual(postEmbedResponse.data)
})

test('$find() handles request without "data" wrapper', async () => {
axiosMock.onGet('http://localhost/posts/1').reply(200, postResponse)

const post = await Post.$find(1)

expect(post).toEqual(postResponse)
})

test('get() method returns a array of objects as instance of suchModel', async () => {
axiosMock.onGet('http://localhost/posts').reply(200, postsResponse)

const posts = await Post.get()
Expand Down Expand Up @@ -95,7 +130,7 @@ describe('Model methods', () => {
post.comments().get()
})

test('$get() fetch style request with "data" attribute', async () => {
test('$get() fetch style request with "data" wrapper', async () => {
axiosMock.onGet('http://localhost/posts').reply(200, postsEmbedResponse)

const posts = await Post.$get()
Expand All @@ -104,7 +139,7 @@ describe('Model methods', () => {

})

test('$get() fetch style request without "data" attribute', async () => {
test('$get() fetch style request without "data" wrapper', async () => {
axiosMock.onGet('http://localhost/posts').reply(200, postsEmbedResponse.data)

const posts = await Post.$get()
Expand Down Expand Up @@ -498,18 +533,18 @@ describe('Model methods', () => {
})

test('it throws a error when a custom() parameter is not a valid Model or a string', () => {

errorModel = () => {
const post = new Post({ text: 'Hello' }).custom()
}

expect(errorModel).toThrow('The custom() method takes a minimum of one argument.')

errorModel = () => {
const user = new User({ name: 'Mary' })
const post = new Post({ text: 'Hello' }).custom(user, 'a-string', 42)
}

expect(errorModel).toThrow('Arguments to custom() must be strings or instances of Model.')
})
})
})